diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4775d01..ce8939f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,12 +2,64 @@ If you wish to contribute to the NanoForge project, fork the repository and submit a pull request. Please mind following the pre-commit hooks to keep the codebase as clean as possible. +## Repository structure + +This repository is a [pnpm workspace](https://pnpm.io/workspaces) monorepo orchestrated with [Turborepo](https://turborepo.com). It contains the following packages: + +| Path | Package | Description | +| ----------------- | --------------------------- | ----------------------------------------------------- | +| `.` | `@nanoforge-dev/cli` | The `nf` command line interface | +| `libs/config` | `@nanoforge-dev/config` | Config types and helpers for `nanoforge.config.ts` | +| `libs/schematics` | `@nanoforge-dev/schematics` | Schematics used by the CLI to generate projects files | + +The CLI depends on the libraries through `workspace:*`, so the libraries must be built before the CLI. The `repo:*` scripts take care of this for you. + ## Setup To get ready to work on the codebase, please do the following: 1. Fork & clone the repository, and make sure you're on the **main** branch -2. Run `pnpm install --frozen-lockfile` ([install](https://pnpm.io/installation)) -3. Make your changes -4. Run `pnpm format && pnpm build && pnpm test:unit` to run ESLint/Prettier, build and tests -5. [Submit a pull request](https://github.com/NanoForge-dev/CLI/compare) (Make sure you follow the [conventional commit format](https://github.com/NanoForge-dev/CLI/blob/main/.github/COMMIT_CONVENTION.md)) +2. Use the Node.js version defined in [`.nvmrc`](../.nvmrc) +3. Run `pnpm install --frozen-lockfile` ([install](https://pnpm.io/installation)) +4. Run `pnpm repo:build` to build every package +5. Make your changes +6. Run `pnpm repo:format && pnpm repo:build && pnpm repo:test` to run ESLint/Prettier, build and tests on every package +7. [Submit a pull request](https://github.com/NanoForge-dev/CLI/compare) (Make sure you follow the [conventional commit format](https://github.com/NanoForge-dev/CLI/blob/main/.github/COMMIT_CONVENTION.md)) + +## Useful commands + +Scripts prefixed with `repo:` run on every package of the workspace through Turborepo: + +| Command | Description | +| --------------------- | --------------------------------------------- | +| `pnpm repo:build` | Build all packages | +| `pnpm repo:lint` | Check formatting and lint all packages | +| `pnpm repo:format` | Format and fix lint errors in all packages | +| `pnpm repo:test` | Run unit and end-to-end tests of all packages | +| `pnpm repo:test:unit` | Run unit tests of all packages | +| `pnpm repo:test:e2e` | Run end-to-end tests of all packages | + +Scripts without prefix (`build`, `lint`, `test`, ...) only run on the CLI package. To run a script on a single library, use the pnpm `--filter` flag: + +```sh +pnpm --filter @nanoforge-dev/config test:unit +pnpm --filter @nanoforge-dev/schematics build +``` + +## Commits + +Commits and pull request titles must follow the [conventional commit format](https://github.com/NanoForge-dev/CLI/blob/main/.github/COMMIT_CONVENTION.md). Use the package as scope when your change targets a single one: `cli`, `config` or `schematics` (e.g. `feat(config): add server port validation`). + +Git hooks are installed automatically by `pnpm install`: + +- `commit-msg` checks the commit message with commitlint +- `pre-commit` formats staged files with lint-staged +- `pre-push` runs `repo:build`, `repo:lint` and `repo:test` + +## Releases + +All the packages of this repository share the same version and are released together. Releases are handled by maintainers through GitHub Actions: + +1. The **Pre-Release** workflow opens a `releases/cli@` pull request that bumps every package to `` and updates their changelogs. The version must be higher than the latest published version of **every** package (e.g. `@nanoforge-dev/schematics` is already at `2.2.1` on npm) +2. Merging this pull request triggers the **Release** workflow, which publishes the packages to npm (libraries first), creates the `` tag and GitHub release, and synchronizes the documentation +3. The **Alpha Release** workflow can be triggered manually to publish an `alpha` pre-release of a single package from any branch diff --git a/.github/ISSUE_TEMPLATE/01-command_bug_report.yml b/.github/ISSUE_TEMPLATE/01-command_bug_report.yml index 30d738b..cf6c03b 100644 --- a/.github/ISSUE_TEMPLATE/01-command_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01-command_bug_report.yml @@ -7,7 +7,17 @@ body: value: | Thank you for filing an issue! If you are here to ask a question, don't hesitate to open a discussion instead: https://github.com/NanoForge-dev/CLI/discussions. - This issue form is for Nanoforge CLI. + This issue form is for the Nanoforge CLI and its libraries (`@nanoforge-dev/config`, `@nanoforge-dev/schematics`). If the issue is not related to a CLI command, pick `other` as the command. + - type: dropdown + id: package + attributes: + label: Which package is this bug report for? + options: + - "@nanoforge-dev/cli" + - "@nanoforge-dev/config" + - "@nanoforge-dev/schematics" + validations: + required: true - type: dropdown id: command attributes: @@ -16,7 +26,7 @@ body: - build - create - dev - - generate + - editor - help - install - login diff --git a/.github/ISSUE_TEMPLATE/02-feature_request.yml b/.github/ISSUE_TEMPLATE/02-feature_request.yml index 9012528..a4f7706 100644 --- a/.github/ISSUE_TEMPLATE/02-feature_request.yml +++ b/.github/ISSUE_TEMPLATE/02-feature_request.yml @@ -6,6 +6,16 @@ body: attributes: value: | Thank you for submitting an idea ! + - type: dropdown + id: package + attributes: + label: Which package is this feature request for? + options: + - "@nanoforge-dev/cli" + - "@nanoforge-dev/config" + - "@nanoforge-dev/schematics" + validations: + required: true - type: dropdown id: command attributes: @@ -14,7 +24,7 @@ body: - build - create - dev - - generate + - editor - help - install - login diff --git a/.github/actions/prepare/action.yml b/.github/actions/prepare/action.yml index 61fe548..facd38a 100644 --- a/.github/actions/prepare/action.yml +++ b/.github/actions/prepare/action.yml @@ -8,7 +8,7 @@ inputs: description: Install pnpm depedencies default: "true" build: - description: Build packages + description: Build all workspace packages default: "true" runs: using: composite @@ -17,7 +17,7 @@ runs: name: Install Node.js uses: actions/setup-node@v6 with: - node-version: 25 + node-version-file: .nvmrc package-manager-cache: false registry-url: https://registry.npmjs.org @@ -28,4 +28,4 @@ runs: - if: inputs.build == 'true' name: Build packages shell: bash - run: pnpm run build + run: pnpm run repo:build diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml index af0aa6e..397fb14 100644 --- a/.github/issue-labeler.yml +++ b/.github/issue-labeler.yml @@ -10,9 +10,9 @@ command:dev: - "### Which command is this (bug report|feature request) for\\?\\n\\ndev\\n" -command:generate: +command:editor: - "### Which command is this (bug - report|feature request) for\\?\\n\\ngenerate\\n" + report|feature request) for\\?\\n\\neditor\\n" command:help: - "### Which command is this (bug @@ -45,3 +45,11 @@ command:start: command:unpublish: - "### Which command is this (bug report|feature request) for\\?\\n\\nunpublish\\n" + +lib:config: + - "### Which package is this (bug + report|feature request) for\\?\\n\\n@nanoforge-dev/config\\n" + +lib:schematics: + - "### Which package is this (bug + report|feature request) for\\?\\n\\n@nanoforge-dev/schematics\\n" diff --git a/.github/labeler.yml b/.github/labeler.yml index 19c2157..89e7091 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -16,11 +16,11 @@ command:dev: - src/command/commands/dev.command.ts - src/action/actions/dev.action.ts -command:generate: +command:editor: - changed-files: - any-glob-to-any-file: - - src/command/commands/generate.command.ts - - src/action/actions/generate.action.ts + - src/command/commands/editor.command.ts + - src/action/actions/editor.action.ts command:install: - changed-files: @@ -69,3 +69,15 @@ documentation: - any-glob-to-any-file: - docs/* - docs/**/* + +lib:config: + - changed-files: + - any-glob-to-any-file: + - libs/config/* + - libs/config/**/* + +lib:schematics: + - changed-files: + - any-glob-to-any-file: + - libs/schematics/* + - libs/schematics/**/* diff --git a/.github/labels.yml b/.github/labels.yml index 3b1e5c3..30ae617 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -54,8 +54,8 @@ description: "Related to dev command" color: "aaa3dc" -- name: "command:generate" - description: "Related to generate command" +- name: "command:editor" + description: "Related to editor command" color: "aaa3dc" - name: "command:help" @@ -90,6 +90,14 @@ description: "Related to unpublish command" color: "aaa3dc" +- name: "lib:config" + description: "Related to the @nanoforge-dev/config library" + color: "c5def5" + +- name: "lib:schematics" + description: "Related to the @nanoforge-dev/schematics library" + color: "c5def5" + - name: "question" description: "Further information is requested" color: "d876e3" diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 587dcfb..2e2de91 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -3,6 +3,14 @@ name: Alpha Release on: workflow_dispatch: inputs: + package: + description: "The published name of a single package to release" + type: choice + required: true + options: + - "@nanoforge-dev/cli" + - "@nanoforge-dev/config" + - "@nanoforge-dev/schematics" dry_run: description: Perform a dry run? type: boolean @@ -16,7 +24,7 @@ jobs: alpha-release: name: Alpha release runs-on: ubuntu-latest - if: github.repository_owner == 'NanoForge-dev' && github.ref_name != 'main' && github.ref_name != 'master' + if: github.repository_owner == 'NanoForge-dev' steps: - name: Checkout repository uses: actions/checkout@v7 @@ -27,7 +35,7 @@ jobs: - name: Release alpha uses: ./node_modules/@nanoforge-dev/actions/actions/release-dev with: - package: "@nanoforge-dev/cli" + package: ${{ inputs.package }} tag: alpha dry: ${{ inputs.dry_run }} env: diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index ce4d8e4..3977f21 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -34,8 +34,10 @@ jobs: - name: Create release PR uses: ./node_modules/@nanoforge-dev/actions/actions/create-packages-release-pr with: - packages: "@nanoforge-dev/cli" + packages: "@nanoforge-dev/config,@nanoforge-dev/schematics,@nanoforge-dev/cli" version: ${{ inputs.version }} + branch-format: "releases/cli@{version}" + commit-format: "chore: release @nanoforge-dev/cli@{version}" dry: ${{ inputs.dry_run }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4a89b7..bee025f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: release: name: Release runs-on: ubuntu-latest - if: github.repository_owner == 'NanoForge-dev' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'releases/') + if: github.repository_owner == 'NanoForge-dev' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'releases/cli@') steps: - name: Checkout repository uses: actions/checkout@v7 @@ -26,33 +26,13 @@ jobs: - name: Release packages uses: ./node_modules/@nanoforge-dev/actions/actions/release-packages with: - packages: "@nanoforge-dev/cli" + packages: "@nanoforge-dev/config,@nanoforge-dev/schematics,@nanoforge-dev/cli" tag-format: "{version}" latest: true env: NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - beta-release: - name: Beta release - runs-on: ubuntu-latest - if: github.repository_owner == 'NanoForge-dev' && github.event.pull_request.merged == true && !startsWith(github.head_ref, 'releases/') - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Prepare - uses: ./.github/actions/prepare - - - name: Release beta - uses: ./node_modules/@nanoforge-dev/actions/actions/release-dev - with: - package: "@nanoforge-dev/cli" - tag: beta - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - publish-docs: name: Publish docs runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a7cdc53..cf20107 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: uses: ./.github/actions/prepare - name: Run linter - run: pnpm run lint + run: pnpm run repo:lint - name: Run tests - run: pnpm run test + run: pnpm run repo:test diff --git a/.gitignore b/.gitignore index d1eb980..af461f3 100644 --- a/.gitignore +++ b/.gitignore @@ -229,4 +229,6 @@ src/**/*.d.ts .tmp-e2e-*/ -.direnv \ No newline at end of file +.direnv + +.turbo/ \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index 842469b..79c9dc2 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,3 @@ -pnpm run build -pnpm run lint -pnpm run test +pnpm run repo:build +pnpm run repo:lint +pnpm run repo:test diff --git a/.nvmrc b/.nvmrc index 7273c0f..4aa0e0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -25 +v26 diff --git a/.prettierignore b/.prettierignore index 921482e..d416fc3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,5 @@ CHANGELOG.md coverage/ .tmp-e2e-*/ + +libs/ diff --git a/README.md b/README.md index 8bf0143..6343fec 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ npm version npm downloads Tests status - Documentation status + Release status Last commit Contributors

@@ -78,7 +78,6 @@ Used to run your nanoforge project in dev mode. - `-d, --directory ` specify the working directory of the command. - `-c, --config ` path to the config file. -- `--generate` generate app files from config, like generate command in dev mode. (default: `false`) ### `editor` @@ -89,15 +88,6 @@ Used to start the nanoforge editor. - `--open` open the editor in the default web browser (default: `true` if path is specified, `false` otherwise). - `--no-open` do not open the editor in the default web browser. -### `generate` - -Used to generate nanoforge project files from config. - -- `-d, --directory ` specify the working directory of the command. -- `-c, --config ` path to the config file. -- `--editor` generate the editor main file. -- `--watch` generate app in watching mode. (default: `false`) - ### `install` or `add` Used to add nanoforge components, systems or libraries to your project. @@ -164,8 +154,8 @@ Used to start your nanoforge project. - `--client-dir ` specify the directory of the client. - `--server-dir ` specify the directory of the server. - `--watch` run app in watching mode. (default: `false`) -- `--cert ` path to the SSL certificate for HTTPS. -- `--key ` path to the SSL key for HTTPS. +- `--cert ` path to the TLS certificate for HTTPS. +- `--key ` path to the TLS key for HTTPS. ### `unpublish` @@ -193,7 +183,7 @@ Variables are scoped by prefix: > **Note:** Prefixes are stripped before the variable is exposed to libraries. -For full documentation on how libraries consume these variables, see [@nanoforge-dev/config](https://github.com/NanoForge-dev/Engine/tree/main/packages/config). +For full documentation on how libraries consume these variables, see [@nanoforge-dev/config](https://github.com/NanoForge-dev/CLI/tree/main/libs/config). ## Contributing diff --git a/cliff.toml b/cliff.toml index 9151523..831f4e2 100644 --- a/cliff.toml +++ b/cliff.toml @@ -76,4 +76,4 @@ sort_commits = "newest" [remote.github] owner = "NanoForge-dev" -repo = "cli" +repo = "CLI" diff --git a/docs/docs/commands/1-overview.mdx b/docs/docs/commands/1-overview.mdx index feff2b4..4d3915f 100644 --- a/docs/docs/commands/1-overview.mdx +++ b/docs/docs/commands/1-overview.mdx @@ -36,19 +36,16 @@ nf --help Serve a built project, optionally over HTTPS. - - Generate project files from your configuration. - - + Create a single component or system. - + Add components, systems, or libraries from the registry. - + Launch the NanoForge visual editor. - + Authenticate, publish, and unpublish packages. diff --git a/docs/docs/commands/2-commands/2-dev.mdx b/docs/docs/commands/2-commands/2-dev.mdx index 022d1a6..af69747 100644 --- a/docs/docs/commands/2-commands/2-dev.mdx +++ b/docs/docs/commands/2-commands/2-dev.mdx @@ -16,11 +16,10 @@ nf dev ## Options -| Option | Description | -| ----------------------------- | ------------------------------------------------------------------------------------------ | -| `-d, --directory ` | Working directory of the command. | -| `-c, --config ` | Path to the configuration file. | -| `--generate` | Generate app files from config in dev mode, like the `generate` command. Default: `false`. | +| Option | Description | +| ----------------------------- | --------------------------------- | +| `-d, --directory ` | Working directory of the command. | +| `-c, --config ` | Path to the configuration file. | ## Examples @@ -29,9 +28,3 @@ nf dev ```bash nf dev ``` - -**Regenerate files from config while developing:** - -```bash -nf dev --generate -``` diff --git a/docs/docs/commands/2-commands/4-start.mdx b/docs/docs/commands/2-commands/4-start.mdx index 1b9e8b2..cf1f49e 100644 --- a/docs/docs/commands/2-commands/4-start.mdx +++ b/docs/docs/commands/2-commands/4-start.mdx @@ -27,8 +27,8 @@ nf start | `--client-dir ` | Directory of the built client. | | `--server-dir ` | Directory of the built server. | | `--watch` | Run in watch mode. Default: `false`. | -| `--cert ` | Path to the SSL certificate for HTTPS. | -| `--key ` | Path to the SSL key for HTTPS. | +| `--cert ` | Path to the TLS certificate for HTTPS. | +| `--key ` | Path to the TLS key for HTTPS. | ## Examples diff --git a/docs/docs/commands/2-commands/6-create.mdx b/docs/docs/commands/2-commands/5-create.mdx similarity index 100% rename from docs/docs/commands/2-commands/6-create.mdx rename to docs/docs/commands/2-commands/5-create.mdx diff --git a/docs/docs/commands/2-commands/5-generate.mdx b/docs/docs/commands/2-commands/5-generate.mdx deleted file mode 100644 index 831fc97..0000000 --- a/docs/docs/commands/2-commands/5-generate.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: generate -description: Generate NanoForge project files from configuration ---- - -## Overview - -`nf generate` generates project files from your [configuration](../../configuration/1-configuration-file). -It reads the config and produces the entry files and wiring your project needs, including the editor -main file when requested. - -## Usage - -```bash -nf generate -``` - -## Options - -| Option | Description | -| ----------------------------- | ----------------------------------------- | -| `-d, --directory ` | Working directory of the command. | -| `-c, --config ` | Path to the configuration file. | -| `--editor` | Generate the editor main file. | -| `--watch` | Generate in watch mode. Default: `false`. | - -## Examples - -**Generate once:** - -```bash -nf generate -``` - -**Regenerate on change, including the editor entry:** - -```bash -nf generate --editor --watch -``` diff --git a/docs/docs/commands/2-commands/7-install.mdx b/docs/docs/commands/2-commands/6-install.mdx similarity index 100% rename from docs/docs/commands/2-commands/7-install.mdx rename to docs/docs/commands/2-commands/6-install.mdx diff --git a/docs/docs/commands/2-commands/8-editor.mdx b/docs/docs/commands/2-commands/7-editor.mdx similarity index 100% rename from docs/docs/commands/2-commands/8-editor.mdx rename to docs/docs/commands/2-commands/7-editor.mdx diff --git a/docs/docs/commands/2-commands/9-registry.mdx b/docs/docs/commands/2-commands/8-registry.mdx similarity index 100% rename from docs/docs/commands/2-commands/9-registry.mdx rename to docs/docs/commands/2-commands/8-registry.mdx diff --git a/docs/docs/configuration/1-configuration-file.mdx b/docs/docs/configuration/1-configuration-file.mdx index 4063f2d..51a89b9 100644 --- a/docs/docs/configuration/1-configuration-file.mdx +++ b/docs/docs/configuration/1-configuration-file.mdx @@ -1,96 +1,140 @@ --- title: Configuration file -description: The nanoforge.config.json file consumed by the CLI +description: The nanoforge.config.ts (or .js) file consumed by the CLI --- import { Tooltip } from "mintlify/components"; ## Overview -Every NanoForge project has a `nanoforge.config.json` at its root. The CLI reads this file to know -how to build, generate, and start your game. Commands accept `-c, --config ` to point at a -different file, and most fields can be overridden per run with command flags. - -## Top-level fields - -| Field | Type | Default | Description | -| --------------- | -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `string` | `nanoforge-app` | The name of the application. | -| `language` | `"ts" \| "js"` | `ts` | Language used in the project. | -| `initFunctions` | `boolean` | `true` | Whether init function lifecycle hooks are wired up. | -| `client` | `object` | - | Client configuration. See below. | -| `server` | `object` | - | Server configuration. See below. | -| `ssl` | `object` | - | Ssl configuration for the game. See below. | - -## `client` - -| Field | Type | Default | Description | -| ----------------- | --------- | ---------------------------------- | -------------------------------------- | -| `enable` | `boolean` | `true` | Whether the client is enabled. | -| `port` | `string` | `3000` | Port the client listens on. | -| `outDir` | `string` | `.nanoforge/client` | Build output directory for the client. | -| `build.entry` | `string` | `client/main.ts` | Client entry file. | -| `build.staticDir` | `string` | `client/static` | Client static assets directory. | -| `editor.entry` | `string` | `.nanoforge/editor/client/main.ts` | Editor entry file for the client. | -| `editor.save` | `string` | `.nanoforge/client.save.json` | Editor save file for the client. | -| `dirs.components` | `string` | `client/components` | Where client components live. | -| `dirs.systems` | `string` | `client/systems` | Where client systems live. | - -## `server` - -The `server` block mirrors `client`, minus `port`. It is disabled by default. - -| Field | Type | Default | Description | -| ----------------- | --------- | ---------------------------------- | -------------------------------------- | -| `enable` | `boolean` | `false` | Whether the server is enabled. | -| `outDir` | `string` | `.nanoforge/server` | Build output directory for the server. | -| `build.entry` | `string` | `server/main.ts` | Server entry file. | -| `build.staticDir` | `string` | `server/static` | Server static assets directory. | -| `editor.entry` | `string` | `.nanoforge/editor/server/main.ts` | Editor entry file for the server. | -| `editor.save` | `string` | `.nanoforge/server.save.json` | Editor save file for the server. | -| `dirs.components` | `string` | `server/components` | Where server components live. | -| `dirs.systems` | `string` | `server/systems` | Where server systems live. | - -## `ssl` - -| Field | Type | Default | Description | -| -------- | -------- | ------- | --------------------------- | -| `enable` | `boolean | `false` | Whether the ssl is enabled. | -| `cert` | `string` | - | Certificate file. | -| `key` | `string` | - | Key file. | - -## Example - -```json -{ - "name": "my-game", - "language": "ts", - "initFunctions": true, - "client": { - "enable": true, - "port": "3000", - "outDir": ".nanoforge/client", - "build": { "entry": "client/main.ts", "staticDir": "client/static" }, - "editor": { - "entry": ".nanoforge/editor/client/main.ts", - "save": ".nanoforge/client.save.json" - }, - "dirs": { "components": "client/components", "systems": "client/systems" } - }, - "server": { - "enable": false, - "outDir": ".nanoforge/server", - "build": { "entry": "server/main.ts", "staticDir": "server/static" }, - "editor": { - "entry": ".nanoforge/editor/server/main.ts", - "save": ".nanoforge/server.save.json" - }, - "dirs": { "components": "server/components", "systems": "server/systems" } - }, - "ssl": { - "enable": true, - "cert": "path/to/cert", - "key": "path/to/key" - } -} +Every NanoForge workspace, project, or lib has a `nanoforge.config.ts` (or `.js`) at its root, +exporting its configuration wrapped in `defineConfig` from `@nanoforge-dev/config`. The CLI reads +this file to know how to build and start your game, and — for a workspace root — to discover the +projects and libs it contains. + +Commands accept `-c, --config ` to point at a different directory to read the config +from (default: the current directory). The CLI looks for `nanoforge.config.ts` in that directory +first, falls back to `nanoforge.config.js` when no `.ts` file exists, and fails if neither is +present. + +## `type` + +A config's `type` field discriminates between four shapes: + +- `workspace` — a monorepo root listing its `packages`. +- `lib` — a shared library consumed by clients/servers. +- `client` — a browser-side NanoForge app. +- `server` — a server-side NanoForge app. + +The config at the root directory the CLI is pointed at must be a `workspace` or a `client`/`server` +project — a bare `lib` config is not valid there. Inside a `workspace`, none of the packages it +resolves may themselves be of type `workspace`; nesting workspaces isn't supported. + +## Defining a config + +Wrap your config in `defineConfig` to get type-checking and editor autocompletion on its fields: + +```ts +import { defineConfig } from "@nanoforge-dev/config"; + +export default defineConfig({ + type: "client", + entryFile: "src/main.ts", + libs: ["../../libs/my-lib"], +}); +``` + +`defineConfig` is an identity function — it returns its argument unchanged. + +## `workspace` + +| Field | Type | Default | Description | +| ---------- | ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages` | `string[]` | `[]` | Glob patterns matched against directories relative to the workspace root, each expected to hold its own `client`, `server`, or `lib` config. | + +A directory matched by a pattern but missing a `nanoforge.config.ts`/`.js` is skipped without +error. A pattern that matches no project or lib at all only logs a warning, rather than failing +the whole resolution. A matched directory whose config is itself of type `workspace` throws. + +### Example + +```ts +import { defineConfig } from "@nanoforge-dev/config"; + +export default defineConfig({ + type: "workspace", + packages: ["apps/*", "libs/*"], +}); +``` + +## `lib` + +| Field | Type | Default | Description | +| ---------------- | -------- | ------------------- | ------------------------ | +| `dir.assets` | `string` | `assets` | Where assets live. | +| `dir.shared` | `string` | `shared` | Where shared code lives. | +| `dir.components` | `string` | `shared/components` | Used by the editor only. | +| `dir.systems` | `string` | `shared/systems` | Used by the editor only. | +| `dir.scenes` | `string` | `shared/scenes` | Used by the editor only. | + +### Example + +```ts +import { defineConfig } from "@nanoforge-dev/config"; + +export default defineConfig({ type: "lib" }); ``` + +## `client` / `server` + +`client` and `server` share the exact same fields and defaults — `type` is the only thing that +tells them apart. + +| Field | Type | Default | Description | +| ---------------- | ---------- | ---------------- | ------------------------------------------------------ | +| `entryFile` | `string` | `src/main.ts` | Entry file. | +| `out.dir` | `string` | `dist` | Build output directory. | +| `out.mainFile` | `string` | `main.js` | Build output entry file name. | +| `dir.assets` | `string` | `assets` | Where assets live. | +| `dir.packages` | `string` | `nf_modules` | Where downloaded package dependencies are installed. | +| `dir.components` | `string` | `src/components` | Used by the editor only. | +| `dir.systems` | `string` | `src/systems` | Used by the editor only. | +| `dir.scenes` | `string` | `src/scenes` | Used by the editor only. | +| `libs` | `string[]` | `[]` | Relative paths to shared libs this project depends on. | + +### Example + +```ts +import { defineConfig } from "@nanoforge-dev/config"; + +export default defineConfig({ + type: "server", + entryFile: "src/main.ts", + dir: { assets: "static" }, + libs: ["../../libs/shared"], +}); +``` + +## Resolving defaults + +Missing fields are filled in with the defaults for the config's `type`, deep-merging nested +objects like `dir` and `out`: + +```ts +import { resolveConfig } from "@nanoforge-dev/config"; + +const resolved = resolveConfig({ type: "client", dir: { assets: "static" } }); +// resolved.dir.assets === "static" +// resolved.dir.packages === "nf_modules" (default, not overridden) +``` + +Array fields (`packages`, `libs`) are replaced wholesale by an override rather than concatenated +with the defaults. + +## Errors + +- `ConfigNotFoundError` — thrown by the CLI when neither `nanoforge.config.ts` nor + `nanoforge.config.js` exists in the directory it was pointed at. +- `ConfigParseError` (from `@nanoforge-dev/config`) — thrown while loading or validating a config + file's default export. Its `code` identifies the failure: `not-found`, `load-failed`, + `no-default-export`, or `invalid-type`. diff --git a/docs/docs/getting-started/1-introduction.mdx b/docs/docs/getting-started/1-introduction.mdx index 57c4ac0..6616b17 100644 --- a/docs/docs/getting-started/1-introduction.mdx +++ b/docs/docs/getting-started/1-introduction.mdx @@ -24,10 +24,7 @@ production, and publishing reusable packages to the NanoForge registry. Run your game locally with hot reloading, then start it as you would in production. - - Generate components, systems, and entry files from your configuration. - - + Publish and consume components, systems, and libraries through the NanoForge registry. diff --git a/docs/docs/getting-started/3-quickstart.mdx b/docs/docs/getting-started/3-quickstart.mdx index a6b52ac..be335af 100644 --- a/docs/docs/getting-started/3-quickstart.mdx +++ b/docs/docs/getting-started/3-quickstart.mdx @@ -60,4 +60,4 @@ This quickstart takes you from an empty folder to a running game in a few minute ## Where to go next - Learn each command in detail in the [Commands reference](../commands/1-overview). -- Follow task-oriented [Guides](../../guides/cli/1-overview) for common workflows. +- Follow task-oriented [Guides](/guides/cli/1-overview) for common workflows. diff --git a/docs/docs/schematics/1-overview.mdx b/docs/docs/schematics/1-overview.mdx new file mode 100644 index 0000000..1b14655 --- /dev/null +++ b/docs/docs/schematics/1-overview.mdx @@ -0,0 +1,52 @@ +--- +title: Schematics +description: Overview of the @nanoforge-dev/schematics package +--- + +import { Card, CardGroup } from "mintlify/components"; + +## What are schematics? + +NanoForge schematics are code generators built on [Angular DevKit](https://github.com/angular/angular-cli/tree/main/packages/angular_devkit/schematics). They produce the boilerplate files that every NanoForge project needs — from the initial application scaffold down to individual ECS components and systems. + +The NanoForge CLI (`nf`) calls these schematics internally whenever you run commands like `nf new` or `nf create`. You can also invoke them directly via the `schematics` CLI for advanced use cases. + +## Installation + +```bash +npm install -g @nanoforge-dev/cli +``` + +The schematics package is a dependency of the CLI and does not need to be installed separately. If you want to run schematics without the CLI: + +```bash +npm install -g @angular-devkit/schematics-cli +npm install @nanoforge-dev/schematics +``` + +Then invoke directly: + +```bash +schematics @nanoforge-dev/schematics: [options] +``` + +## Available schematics + + + + Scaffold the root of a NanoForge monorepo: `nanoforge.config.ts`, `package.json`, TypeScript or + JavaScript config, and linting. + + + Generate a client or server project under `apps/` inside a workspace, including its + `main.ts` entry point, example component/system, and optional Dockerfile. + + + Scaffold an ECS component class with an editor manifest for use in the NanoForge editor. + + + Scaffold an ECS system function with an editor manifest for use in the NanoForge editor. + + + +see the [changelog](https://github.com/NanoForge-dev/schematics/blob/main/CHANGELOG.md) for the full release history. diff --git a/docs/docs/schematics/2-schematics/1-workspace.mdx b/docs/docs/schematics/2-schematics/1-workspace.mdx new file mode 100644 index 0000000..ce94053 --- /dev/null +++ b/docs/docs/schematics/2-schematics/1-workspace.mdx @@ -0,0 +1,69 @@ +--- +title: Workspace +description: Generate a NanoForge workspace (monorepo root) +--- + +import { Tooltip } from "mintlify/components"; + +## Overview + +The `workspace` schematic creates the root of a NanoForge monorepo: the `nanoforge.config.ts` workspace declaration, `package.json`, and TypeScript or JavaScript base config. A workspace holds one or more projects generated with the [`project`](./2-project) schematic under `apps/`. + +`@nanoforge-dev/cli`, `nanoforge` and (for TypeScript) `typescript` are pinned to the newest registry version published at least 48h ago at generation time, mirroring pnpm's own `minimumReleaseAge` supply-chain protection so a freshly (and potentially compromised) published release isn't picked up immediately. Non-nanoforge packages like `typescript` are pinned to a specific version rather than a range; `typescript` is also kept on its `6.x` line rather than jumping to the `7.x` native rewrite. + +## Usage + +```bash +schematics @nanoforge-dev/schematics:workspace --name=my-game +``` + +## Options + +| Option | Type | Default | Description | +| ---------------- | ------------------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | `nanoforge-workspace` | The name of the workspace | +| `directory` | `string` | `name` | Destination directory for the generated files | +| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files | +| `strict` | `boolean` | `true` | Enable strict mode | +| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target | +| `allowBuilds` | `string[]` | `[]` | Packages allowed to run install/build scripts, in addition to `bun` | + +## Generated structure + +``` +my-game/ +├── package.json +├── tsconfig.json # TypeScript only +├── tsconfig.spec.json # TypeScript only +├── jsconfig.json # JavaScript only +├── pnpm-workspace.yaml # if packageManager: pnpm — allowBuilds map +├── nanoforge.config.ts # (or .js) +├── .gitignore +├── README.md +└── apps/ # generated with the `project` schematic +``` + +## Examples + +**Minimal TypeScript workspace:** + +```bash +schematics @nanoforge-dev/schematics:workspace --name=my-game +``` + +**pnpm workspace:** + +```bash +schematics @nanoforge-dev/schematics:workspace \ + --name=my-game \ + --packageManager=pnpm +``` + +**pnpm workspace allowing an extra dependency's build scripts (bun is always allowed):** + +```bash +schematics @nanoforge-dev/schematics:workspace \ + --name=my-game \ + --packageManager=pnpm \ + --allowBuilds=esbuild +``` diff --git a/docs/docs/schematics/2-schematics/2-project.mdx b/docs/docs/schematics/2-schematics/2-project.mdx new file mode 100644 index 0000000..dcd7c79 --- /dev/null +++ b/docs/docs/schematics/2-schematics/2-project.mdx @@ -0,0 +1,100 @@ +--- +title: Project +description: Generate a NanoForge client or server project inside a workspace +--- + +import { Tooltip } from "mintlify/components"; + +## Overview + +The `project` schematic generates a complete client or server project under `directory` (e.g. `apps/client`) inside a [`workspace`](./1-workspace): its own `package.json`, `nanoforge.config.ts`, `tsconfig.json`, example ECS component and system, `src/main.ts` entry point, and optionally a `Dockerfile` and lifecycle init functions. `directory` is the full destination path — it isn't appended with `part` automatically, so generating a client and a server side by side means passing e.g. `--directory=apps/client` and `--directory=apps/server` in two separate invocations. + +For a standalone project (`workspace: false`), engine dependency versions are pinned to the newest release published at least 48h ago, mirroring pnpm's own `minimumReleaseAge` supply-chain protection. `@nanoforge-dev/ecs` and `@nanoforge-dev/network` are resolved directly under those same names — no aliasing to a different registry package. A standalone project also generates its own `allowBuilds` config (`pnpm-workspace.yaml`'s `allowBuilds` map, `package.json`'s `allowScripts` or `trustedDependencies`, depending on `packageManager`) since it has no parent workspace root to provide one — see `allowBuilds` below. + +Engine packages are declared as `devDependencies`, except `@nanoforge-dev/network` on a server project, which is declared in `dependencies` because the server needs it at runtime. A client project gets `@nanoforge-dev/graphics-2d`, `@nanoforge-dev/input` and `@nanoforge-dev/network` in addition to `@nanoforge-dev/ecs` and `nanoforge`. + +## Usage + +```bash +schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-game --directory=apps/client +``` + +## Options + +| Option | Type | Default | Description | +| ---------------- | ------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `part` | `"client" \| "server"` | _(required)_ | The type of project to generate | +| `workspaceName` | `string` | _(required)_ | Name of the parent workspace | +| `directory` | `string` | `apps` | Full destination path for this project | +| `language` | `"ts" \| "js"` | `ts` | Language to use for generated source files | +| `strict` | `boolean` | `true` | Enable strict mode | +| `packageManager` | `"npm" \| "yarn" \| "pnpm" \| "bun"` | `npm` | Package manager to target | +| `workspace` | `boolean` | `true` | Generated inside an existing NanoForge workspace | +| `initFunctions` | `boolean` | `false` | Add lifecycle init function hooks | +| `hasServer` | `boolean` | `false` | Sync the client's position with a companion server | +| `docker` | `boolean` | `false` | Generate a Dockerfile and .dockerignore | +| `editor` | `boolean` | `false` | Reserved for future editor build support | +| `libs` | `string[]` | `[]` | Relative paths to shared libs this project depends on, written into `nanoforge.config.ts` | +| `allowBuilds` | `string[]` | `[]` | Packages allowed to run install/build scripts (standalone projects only) | + +## Generated structure + +``` +/ +├── package.json +├── tsconfig.json # TypeScript only +├── jsconfig.json # JavaScript only +├── nanoforge.config.ts # (or .js) +├── pnpm-workspace.yaml # if !workspace && packageManager: pnpm && allowBuilds.length > 0 +├── Dockerfile # if docker: true +├── .dockerignore # if docker: true +├── src/ +│ ├── main.ts +│ ├── components/ +│ │ ├── position-2d.component.ts # both parts +│ │ └── drawable-circle-2d.component.ts # client only +│ └── systems/ +│ ├── draw-2d.system.ts # client only +│ ├── position-sync.system.ts # client only, if hasServer: true +│ └── move-2d.system.ts # server only +└── assets/ +``` + +## Examples + +**Generate a client project:** + +```bash +schematics @nanoforge-dev/schematics:project --part=client --workspaceName=my-game --directory=apps/client +``` + +**Generate a server project with Docker, syncing position with the client:** + +```bash +schematics @nanoforge-dev/schematics:project \ + --part=server \ + --workspaceName=my-game \ + --directory=apps/server \ + --docker=true +``` + +**Generate a client that syncs its position from that server:** + +```bash +schematics @nanoforge-dev/schematics:project \ + --part=client \ + --workspaceName=my-game \ + --directory=apps/client \ + --hasServer=true +``` + +**Generate a JavaScript client depending on a shared lib:** + +```bash +schematics @nanoforge-dev/schematics:project \ + --part=client \ + --workspaceName=my-game \ + --directory=apps/client \ + --language=js \ + --libs=../../libs/shared +``` diff --git a/docs/docs/schematics/2-schematics/3-component.mdx b/docs/docs/schematics/2-schematics/3-component.mdx new file mode 100644 index 0000000..9b14080 --- /dev/null +++ b/docs/docs/schematics/2-schematics/3-component.mdx @@ -0,0 +1,120 @@ +--- +title: Component +description: Generate a NanoForge ECS component +--- + +import Tooltip from "mint/Tooltip"; + +## Overview + +The `component` schematic scaffolds a new ECS component class for a NanoForge client or server part. The generated file includes the component class, a default export required for code generation, and an `EDITOR_COMPONENT_MANIFEST` required for the NanoForge editor to display and configure the component. + +## Usage + +```bash +schematics @nanoforge-dev/schematics:component --part=client --name=health +``` + +## Options + +| Option | Type | Default | Description | +| ----------- | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | `example` | Component name | +| `part` | `"client" \| "server"` | _(required)_ | The part of the application this component belongs to | +| `directory` | `string` | `.` | Destination directory for the generated file | +| `language` | `"ts" \| "js"` | `ts` | Language to use for the generated file | + +## Generated file + +`{directory}/{name}.component.ts` + +```ts +import { type EditorComponentManifest } from "@nanoforge-dev/ecs-client"; + +export class HealthComponent { + name = this.constructor.name; + + constructor( + public paramA: string, + public paramB: number, + public paramC: boolean = false, + ) {} + + get foo() { + return "bar"; + } + + get paramAOrDefault() { + return this.paramC ? this.paramA : "default"; + } + + addOne() { + this.paramB += 1; + } +} + +// Required to generate code +export default HealthComponent.name; + +// Required for the editor to display the component and generate code +export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = { + name: "Health", + description: "Health component description", + params: [ + { + type: "string", + name: "paramA", + description: "Param A description", + example: "Example value", + }, + { + type: "number", + name: "paramB", + description: "Param B description", + example: 3, + }, + { + type: "boolean", + name: "paramC", + description: "Param C description", + example: true, + default: false, + optional: true, + }, + ], +}; +``` + +## Editor manifest + +The `EDITOR_COMPONENT_MANIFEST` export is required for the NanoForge editor to discover the component, render its configuration panel, and generate code. Each `params` entry describes one constructor parameter: + +| Field | Required | Description | +| ------------- | -------- | ------------------------------------------------------------ | +| `type` | Yes | Parameter type: `"string"`, `"number"`, or `"boolean"` | +| `name` | Yes | Constructor parameter name | +| `description` | Yes | Human-readable description shown in the editor | +| `example` | Yes | Example value used in code generation | +| `default` | No | Default value when the parameter is optional | +| `optional` | No | When `true`, the editor treats the parameter as non-required | + +## Examples + +**Generate a `PlayerComponent` for the client:** + +```bash +schematics @nanoforge-dev/schematics:component \ + --part=client \ + --name=player \ + --directory=client/components +``` + +**Generate a JavaScript server component:** + +```bash +schematics @nanoforge-dev/schematics:component \ + --part=server \ + --name=network-player \ + --language=js \ + --directory=server/components +``` diff --git a/docs/docs/schematics/2-schematics/4-system.mdx b/docs/docs/schematics/2-schematics/4-system.mdx new file mode 100644 index 0000000..51ecf8d --- /dev/null +++ b/docs/docs/schematics/2-schematics/4-system.mdx @@ -0,0 +1,105 @@ +--- +title: System +description: Generate a NanoForge ECS system +--- + +import { Tooltip } from "mintlify/components"; + +## Overview + +The `system` schematic scaffolds a new ECS system function for a NanoForge client or server part. The generated file includes the system function, a default export required for code generation, and an `EDITOR_SYSTEM_MANIFEST` required for the NanoForge editor to display and configure the system. + +## Usage + +```bash +schematics @nanoforge-dev/schematics:system --part=client --name=movement +``` + +## Options + +| Option | Type | Default | Description | +| ----------- | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | `string` | `example` | System name | +| `part` | `"client" \| "server"` | _(required)_ | The part of the application this system belongs to | +| `directory` | `string` | `.` | Destination directory for the generated file | +| `language` | `"ts" \| "js"` | `ts` | Language to use for the generated file | + +## Generated file + +`{directory}/{name}.system.ts` + +```ts +import { type Context } from "@nanoforge-dev/common"; +import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-client"; + +import { ExampleComponent } from "../components/example.component"; + +export const movementSystem = (registry: Registry, ctx: Context) => { + const entities = registry.getZipper([ExampleComponent]); + + entities.forEach((entity) => { + if (entity.ExampleComponent.paramA === "end") { + ctx.app.setIsRunning(false); + return; + } + + if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end"; + + if (entity.ExampleComponent.paramB >= 0) + entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1; + }); +}; + +// Required to generate code +export default movementSystem.name; + +// Required for the editor to display the system and generate code +export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = { + name: "movement", + description: + "This system end the game when paramB reaches 0 for any entity with ExampleComponent", + dependencies: ["ExampleComponent"], +}; +``` + +## Editor manifest + +The `EDITOR_SYSTEM_MANIFEST` export is required for the NanoForge editor to discover the system and wire it into the game loop. + +| Field | Required | Description | +| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Yes | System name as displayed in the editor | +| `description` | Yes | Human-readable description of what the system does | +| `dependencies` | Yes | Component names this system depends on | + +## System signature + +Every NanoForge system follows the same function signature: + +```ts +(registry: Registry, ctx: Context) => void +``` + +- **`registry`** — the ECS registry; use `registry.getZipper([...])` to query entities with specific components. +- **`ctx`** — the application context; provides access to `ctx.app`, `ctx.assets`, and other engine services. + +## Examples + +**Generate a `PhysicsSystem` for the client:** + +```bash +schematics @nanoforge-dev/schematics:system \ + --part=client \ + --name=physics \ + --directory=client/systems +``` + +**Generate a JavaScript server system:** + +```bash +schematics @nanoforge-dev/schematics:system \ + --part=server \ + --name=sync-state \ + --language=js \ + --directory=server/systems +``` diff --git a/docs/guides/cli/1-overview.mdx b/docs/guides/cli/1-overview.mdx index eec3f3a..0f987de 100644 --- a/docs/guides/cli/1-overview.mdx +++ b/docs/guides/cli/1-overview.mdx @@ -5,7 +5,7 @@ description: Task-oriented walkthroughs for common NanoForge CLI workflows import { Card, CardGroup } from "mintlify/components"; -The [Commands reference](../../docs/commands/1-overview) documents what each command does. These guides +The [Commands reference](/docs/commands/1-overview) documents what each command does. These guides take the opposite angle: they follow the **paths a user actually walks** when building a game with the CLI, stitching commands together end to end. diff --git a/docs/guides/cli/3-develop-locally.mdx b/docs/guides/cli/3-develop-locally.mdx index e474f61..3dcda03 100644 --- a/docs/guides/cli/3-develop-locally.mdx +++ b/docs/guides/cli/3-develop-locally.mdx @@ -18,22 +18,6 @@ Once a project exists, most of your time is spent in the develop-and-iterate loo - - When you change `nanoforge.config.json` - for example adding new component or system directories - - regenerate the project files. You can do this in a one-off run: - - ```bash - nf generate - ``` - - Or have dev mode regenerate as it runs: - - ```bash - nf dev --generate - ``` - - - Working in a monorepo or with multiple configs? Every command accepts `-d` and `-c`: diff --git a/e2e/cli-build.test.ts b/e2e/cli-build.test.ts index 82da5d5..7a917ab 100644 --- a/e2e/cli-build.test.ts +++ b/e2e/cli-build.test.ts @@ -2,10 +2,29 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { stripDefineConfig, writeStandaloneEntry } from "./helpers/project-fixtures"; import { runCli } from "./helpers/run-cli"; const tmpDir = resolve(__dirname, "../.tmp-e2e-build"); +const newProject = (name: string, directory: string, server: boolean) => + runCli([ + "new", + "--name", + name, + "--language", + "ts", + "--package-manager", + "npm", + "--strict", + server ? "--server" : "--no-server", + "--skip-install", + "--no-docker", + "--no-git", + "-d", + directory, + ]); + beforeAll(async () => { mkdirSync(tmpDir, { recursive: true }); }); @@ -17,65 +36,48 @@ afterAll(() => { describe("nf build (TypeScript, no server)", () => { const projectDir = resolve(tmpDir, "build-ts-no-server"); const appDir = resolve(projectDir, "build-app"); + const outDir = resolve(appDir, "dist"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "build-app", - "--language", - "ts", - "--package-manager", - "npm", - "--strict", - "--no-server", - "--no-init-functions", - "--no-skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); + await newProject("build-app", projectDir, false); + stripDefineConfig(appDir); + writeStandaloneEntry(appDir); }); it("should run the build command", async () => { const { exitCode } = await runCli(["build", "-d", appDir]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "client"))).toBe(true); - expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true); + expect(existsSync(resolve(outDir, "main.js"))).toBe(true); }); - it("should accept --config option", async () => { - const { exitCode } = await runCli(["build", "-d", appDir, "--config", "nanoforge.config.json"]); + it("should accept --client-out-dir option", async () => { + const { exitCode } = await runCli(["build", "-d", appDir, "--client-out-dir", "custom-out"]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true); + expect(existsSync(resolve(appDir, "custom-out", "main.js"))).toBe(true); }); - it("should accept --client-out-dir option", async () => { - const { exitCode } = await runCli(["build", "-d", appDir, "--client-out-dir", "custom-out"]); + it("should accept --client-entry option", async () => { + writeFileSync(resolve(appDir, "src", "other.ts"), 'console.log("other");\n'); + + const { exitCode } = await runCli(["build", "-d", appDir, "--client-entry", "src/other.ts"]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, "custom-out"))).toBe(true); - expect(existsSync(resolve(appDir, "custom-out", "main.js"))).toBe(true); + expect(existsSync(resolve(outDir, "other.js"))).toBe(true); }); - it("should copy static files to output dir", async () => { - const staticDir = resolve(appDir, "client", "static"); - mkdirSync(staticDir, { recursive: true }); - writeFileSync(resolve(staticDir, "asset.txt"), "hello"); + it("should copy assets to output dir", async () => { + writeFileSync(resolve(appDir, "assets", "asset.txt"), "hello"); const { exitCode } = await runCli(["build", "-d", appDir]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "client", "asset.txt"))).toBe(true); + expect(existsSync(resolve(outDir, "asset.txt"))).toBe(true); }); it("should reset output dir before rebuild", async () => { - const outDir = resolve(appDir, ".nanoforge", "client"); mkdirSync(outDir, { recursive: true }); writeFileSync(resolve(outDir, "stale.js"), "// stale"); @@ -100,97 +102,89 @@ describe("nf build (TypeScript, no server)", () => { ]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "client", "custom-asset.txt"))).toBe(true); + expect(existsSync(resolve(outDir, "custom-asset.txt"))).toBe(true); }); it("should keep process alive with --watch flag", async () => { const { killed } = await runCli(["build", "-d", appDir, "--watch"], { timeout: 3000 }); - expect(killed).toBe(true); }); it("should accept --editor flag and use editor entry", async () => { - const editorEntryDir = resolve(appDir, ".nanoforge", "editor", "client"); - mkdirSync(editorEntryDir, { recursive: true }); - writeFileSync(resolve(editorEntryDir, "main.ts"), 'console.log("editor");'); - const { exitCode } = await runCli(["build", "-d", appDir, "--editor"]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true); + expect(existsSync(resolve(outDir, "main.js"))).toBe(true); }); }); describe("nf build (TypeScript, with server)", () => { const projectDir = resolve(tmpDir, "build-ts-with-server"); - const appDir = resolve(projectDir, "build-server-app"); + const workspaceDir = resolve(projectDir, "build-server-app"); + const clientDir = resolve(workspaceDir, "apps/client"); + const serverDir = resolve(workspaceDir, "apps/server"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); + await newProject("build-server-app", projectDir, true); + for (const dir of [workspaceDir, clientDir, serverDir]) stripDefineConfig(dir); + writeStandaloneEntry(clientDir); + writeStandaloneEntry(serverDir); + }); - await runCli([ - "new", - "--name", - "build-server-app", - "--language", - "ts", - "--package-manager", - "npm", - "--no-strict", - "--server", - "--no-init-functions", - "--no-skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); + it("should build every project of the workspace", async () => { + const { exitCode } = await runCli(["build", "-d", workspaceDir]); + + expect(exitCode).toBe(0); + expect(existsSync(resolve(clientDir, "dist", "main.js"))).toBe(true); + expect(existsSync(resolve(serverDir, "dist", "main.js"))).toBe(true); }); - it("should run the build command with server enabled", async () => { - const { exitCode } = await runCli(["build", "-d", appDir]); + it("should build a single project of the workspace", async () => { + rmSync(resolve(clientDir, "dist"), { recursive: true, force: true }); + + const { exitCode } = await runCli(["build", "-d", serverDir]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "client", "main.js"))).toBe(true); - expect(existsSync(resolve(appDir, ".nanoforge", "server", "main.js"))).toBe(true); + expect(existsSync(resolve(serverDir, "dist", "main.js"))).toBe(true); + expect(existsSync(resolve(clientDir, "dist"))).toBe(false); }); it("should accept --server-out-dir option", async () => { const { exitCode } = await runCli([ "build", "-d", - appDir, + workspaceDir, "--server-out-dir", "custom-server-out", ]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, "custom-server-out"))).toBe(true); - expect(existsSync(resolve(appDir, "custom-server-out", "main.js"))).toBe(true); + expect(existsSync(resolve(serverDir, "custom-server-out", "main.js"))).toBe(true); + expect(existsSync(resolve(clientDir, "dist", "main.js"))).toBe(true); }); it("should accept --server-static-dir option and copy files from it", async () => { - const customStaticDir = resolve(appDir, "custom-server-static"); + const customStaticDir = resolve(serverDir, "custom-server-static"); mkdirSync(customStaticDir, { recursive: true }); writeFileSync(resolve(customStaticDir, "server-asset.txt"), "server"); const { exitCode } = await runCli([ "build", "-d", - appDir, + workspaceDir, "--server-static-dir", "custom-server-static", ]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, ".nanoforge", "server", "server-asset.txt"))).toBe(true); + expect(existsSync(resolve(serverDir, "dist", "server-asset.txt"))).toBe(true); }); }); describe("nf build (with invalid directory)", () => { it("should fail when directory does not exist", async () => { const { exitCode } = await runCli(["build", "-d", resolve(tmpDir, "nonexistent")]); - expect(exitCode).not.toBe(0); }); @@ -199,7 +193,6 @@ describe("nf build (with invalid directory)", () => { mkdirSync(emptyDir, { recursive: true }); const { exitCode } = await runCli(["build", "-d", emptyDir]); - expect(exitCode).not.toBe(0); }); }); diff --git a/e2e/cli-create.test.ts b/e2e/cli-create.test.ts index 4e09265..cb88640 100644 --- a/e2e/cli-create.test.ts +++ b/e2e/cli-create.test.ts @@ -2,10 +2,29 @@ import { existsSync, mkdirSync, rmSync } from "node:fs"; import { resolve } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { stripDefineConfig } from "./helpers/project-fixtures"; import { runCli } from "./helpers/run-cli"; const tmpDir = resolve(__dirname, "../.tmp-e2e-create"); +const newProject = (name: string, directory: string, language: "ts" | "js", server: boolean) => + runCli([ + "new", + "--name", + name, + "--language", + language, + "--package-manager", + "npm", + "--no-strict", + server ? "--server" : "--no-server", + "--skip-install", + "--no-docker", + "--no-git", + "-d", + directory, + ]); + beforeAll(() => { mkdirSync(tmpDir, { recursive: true }); }); @@ -14,30 +33,14 @@ afterAll(() => { rmSync(tmpDir, { recursive: true, force: true }); }); -describe("nf create (TypeScript, client component)", () => { +describe("nf create (TypeScript, client project)", () => { const projectDir = resolve(tmpDir, "create-ts-client"); const appDir = resolve(projectDir, "create-app"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "create-app", - "--language", - "ts", - "--package-manager", - "npm", - "--strict", - "--no-server", - "--no-init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); + await newProject("create-app", projectDir, "ts", false); + stripDefineConfig(appDir); }); it("should create a component successfully", async () => { @@ -55,8 +58,8 @@ describe("nf create (TypeScript, client component)", () => { expect(stdout).toContain("Element successfully created"); }); - it("should generate a component file in client/components", () => { - expect(existsSync(resolve(appDir, "client/components/player.component.ts"))).toBe(true); + it("should generate a component file in src/components", () => { + expect(existsSync(resolve(appDir, "src/components/player.component.ts"))).toBe(true); }); it("should create a system successfully", async () => { @@ -73,23 +76,8 @@ describe("nf create (TypeScript, client component)", () => { expect(stdout).toContain("Element successfully created"); }); - it("should generate a system file in client/systems", () => { - expect(existsSync(resolve(appDir, "client/systems/movement.system.ts"))).toBe(true); - }); - - it("should accept --config option", async () => { - const { exitCode } = await runCli([ - "create", - "component", - "--name", - "health", - "--config", - "nanoforge.config.json", - "-d", - appDir, - ]); - - expect(exitCode).toBe(0); + it("should generate a system file in src/systems", () => { + expect(existsSync(resolve(appDir, "src/systems/movement.system.ts"))).toBe(true); }); it("should create a component with a custom path via --path", async () => { @@ -99,40 +87,26 @@ describe("nf create (TypeScript, client component)", () => { "--name", "custom", "--path", - "client/custom-components", + "src/custom-components", "-d", appDir, ]); expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, "client/custom-components/custom.component.ts"))).toBe(true); + expect(existsSync(resolve(appDir, "src/custom-components/custom.component.ts"))).toBe(true); }); }); -describe("nf create (TypeScript, server component)", () => { +describe("nf create (TypeScript, server project in a workspace)", () => { const projectDir = resolve(tmpDir, "create-ts-server"); - const appDir = resolve(projectDir, "create-server-app"); + const workspaceDir = resolve(projectDir, "create-server-app"); + const serverDir = resolve(workspaceDir, "apps/server"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "create-server-app", - "--language", - "ts", - "--package-manager", - "npm", - "--no-strict", - "--server", - "--no-init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); + await newProject("create-server-app", projectDir, "ts", true); + stripDefineConfig(workspaceDir); + stripDefineConfig(serverDir); }); it("should create a server component successfully", async () => { @@ -140,52 +114,34 @@ describe("nf create (TypeScript, server component)", () => { "create", "component", "--name", - "enemy", - "--server", + "network", "-d", - appDir, + serverDir, ]); expect(exitCode).toBe(0); - }); - - it("should generate a component file in server/components", () => { - expect(existsSync(resolve(appDir, "server/components/enemy.component.ts"))).toBe(true); + expect(existsSync(resolve(serverDir, "src/components/network.component.ts"))).toBe(true); }); it("should create a server system successfully", async () => { - const { exitCode } = await runCli([ - "create", - "system", - "--name", - "physics", - "--server", - "-d", - appDir, - ]); + const { exitCode } = await runCli(["create", "system", "--name", "sync", "-d", serverDir]); expect(exitCode).toBe(0); + expect(existsSync(resolve(serverDir, "src/systems/sync.system.ts"))).toBe(true); }); - it("should generate a system file in server/systems", () => { - expect(existsSync(resolve(appDir, "server/systems/physics.system.ts"))).toBe(true); - }); - - it("should create a server component with a custom path via --path", async () => { + it("should fail when run at the workspace root", async () => { const { exitCode } = await runCli([ "create", "component", "--name", - "network", - "--server", - "--path", - "server/custom-components", + "root", "-d", - appDir, + workspaceDir, ]); - expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, "server/custom-components/network.component.ts"))).toBe(true); + expect(exitCode).not.toBe(0); + expect(existsSync(resolve(workspaceDir, "src/components/root.component.ts"))).toBe(false); }); }); @@ -195,44 +151,26 @@ describe("nf create (JavaScript)", () => { beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "create-js-app", - "--language", - "js", - "--package-manager", - "npm", - "--no-strict", - "--no-server", - "--no-init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); + await newProject("create-js-app", projectDir, "js", false); + stripDefineConfig(appDir); }); it("should create a JavaScript component successfully", async () => { const { exitCode } = await runCli(["create", "component", "--name", "sprite", "-d", appDir]); - expect(exitCode).toBe(0); }); it("should generate a .js component file", () => { - expect(existsSync(resolve(appDir, "client/components/sprite.component.js"))).toBe(true); + expect(existsSync(resolve(appDir, "src/components/sprite.component.js"))).toBe(true); }); it("should create a JavaScript system successfully", async () => { const { exitCode } = await runCli(["create", "system", "--name", "render", "-d", appDir]); - expect(exitCode).toBe(0); }); it("should generate a .js system file", () => { - expect(existsSync(resolve(appDir, "client/systems/render.system.js"))).toBe(true); + expect(existsSync(resolve(appDir, "src/systems/render.system.js"))).toBe(true); }); }); @@ -246,16 +184,12 @@ describe("nf create (error cases)", () => { "-d", resolve(tmpDir, "nonexistent"), ]); - expect(exitCode).not.toBe(0); }); it("should fail with an invalid type", async () => { - const projectDir = resolve(tmpDir, "create-ts-client"); - const appDir = resolve(projectDir, "create-app"); - + const appDir = resolve(tmpDir, "create-ts-client/create-app"); const { exitCode } = await runCli(["create", "invalid-type", "--name", "test", "-d", appDir]); - expect(exitCode).not.toBe(0); }); }); diff --git a/e2e/cli-generate.test.ts b/e2e/cli-generate.test.ts deleted file mode 100644 index 05d74a9..0000000 --- a/e2e/cli-generate.test.ts +++ /dev/null @@ -1,355 +0,0 @@ -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { resolve } from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; - -import { runCli } from "./helpers/run-cli"; - -const tmpDir = resolve(__dirname, "../.tmp-e2e-generate"); - -beforeAll(async () => { - mkdirSync(tmpDir, { recursive: true }); -}); - -afterAll(() => { - rmSync(tmpDir, { recursive: true, force: true }); -}); - -describe("nf generate (TypeScript, no server)", () => { - const projectDir = resolve(tmpDir, "gen-ts-no-server"); - const appDir = resolve(projectDir, "gen-app"); - - beforeAll(async () => { - mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "gen-app", - "--language", - "ts", - "--package-manager", - "npm", - "--strict", - "--no-server", - "--init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); - }); - - it("should run the generate command", async () => { - const { exitCode } = await runCli(["generate", "-d", appDir]); - - expect(exitCode).toBe(0); - }); - - it("should accept --config option", async () => { - const { exitCode } = await runCli([ - "generate", - "-d", - appDir, - "--config", - "nanoforge.config.json", - ]); - - expect(exitCode).toBe(0); - }); - - it("should generate client main.ts file", async () => { - const { exitCode } = await runCli(["generate", "-d", appDir]); - - expect(exitCode).toBe(0); - expect(existsSync(resolve(appDir, "client/main.ts"))).toBe(true); - }); - - it("should generate valid TypeScript client main content", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8"); - - // Core imports - expect(content).toContain('import { type IRunOptions } from "@nanoforge-dev/common"'); - expect(content).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core"'); - - // Library imports - expect(content).toContain('import { AssetManagerLibrary } from "@nanoforge-dev/asset-manager"'); - expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"'); - expect(content).toContain('import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d"'); - expect(content).toContain('import { InputLibrary } from "@nanoforge-dev/input"'); - expect(content).toContain('import { MusicLibrary } from "@nanoforge-dev/music"'); - expect(content).toContain('import { SoundLibrary } from "@nanoforge-dev/sound"'); - - // Main function signature with TypeScript type - expect(content).toContain("export async function main(options: IRunOptions)"); - - // Factory creates client - expect(content).toContain("NanoforgeFactory.createClient()"); - - // App lifecycle - expect(content).toContain("await app.init(options)"); - expect(content).toContain("await app.run()"); - }); - - it("should generate init function imports and calls", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8"); - - // Init function imports - expect(content).toContain('import { afterInit } from "./init/after-init"'); - expect(content).toContain('import { beforeInit } from "./init/before-init"'); - expect(content).toContain('import { afterRegistryInit } from "./init/after-registry-init"'); - expect(content).toContain('import { beforeRegistryInit } from "./init/before-registry-init"'); - expect(content).toContain('import { afterRun } from "./init/after-run"'); - expect(content).toContain('import { beforeRun } from "./init/before-run"'); - - // Init function calls - expect(content).toContain("await beforeInit(app)"); - expect(content).toContain("await afterInit(app)"); - expect(content).toContain("await beforeRun(app)"); - expect(content).toContain("await afterRun(app)"); - expect(content).toContain("await beforeRegistryInit(app, registry)"); - expect(content).toContain("await afterRegistryInit(app, registry)"); - }); - - it("should generate library instantiation and registration", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8"); - - // Library instantiation - expect(content).toContain("const assetManagerLibrary = new AssetManagerLibrary()"); - expect(content).toContain("const ecsLibrary = new ECSClientLibrary()"); - expect(content).toContain("const graphicsLibrary = new Graphics2DLibrary()"); - expect(content).toContain("const inputLibrary = new InputLibrary()"); - expect(content).toContain("const musicLibrary = new MusicLibrary()"); - expect(content).toContain("const soundLibrary = new SoundLibrary()"); - - // Library registration - expect(content).toContain("app.useAssetManager(assetManagerLibrary)"); - expect(content).toContain("app.useComponentSystem(ecsLibrary)"); - expect(content).toContain("app.useGraphics(graphicsLibrary)"); - expect(content).toContain("app.useInput(inputLibrary)"); - expect(content).toContain("app.useSound(soundLibrary)"); - }); - - it("should generate ECS components, systems, and entities", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8"); - - // Component and system imports - expect(content).toContain('import { ExampleComponent } from "./components/example.component"'); - expect(content).toContain('import { exampleSystem } from "./systems/example.system"'); - - // Registry access - expect(content).toContain("const registry = ecsLibrary.registry"); - - // Entity creation and component attachment - expect(content).toContain("const exampleEntity = registry.spawnEntity()"); - expect(content).toContain( - 'registry.addComponent(exampleEntity, new ExampleComponent("example", 10, undefined))', - ); - - // System registration - expect(content).toContain("registry.addSystem(exampleSystem)"); - }); - - it("should regenerate main.ts with modified save file (add component and entity)", async () => { - const savePath = resolve(appDir, ".nanoforge/client.save.json"); - const save = JSON.parse(readFileSync(savePath, "utf-8")); - - save.components.push({ - name: "HealthComponent", - path: "./components/health.component.ts", - paramsNames: ["health"], - }); - - save.entities[0].components["HealthComponent"] = { health: 100 }; - - save.entities.push({ - id: "exampleEntity2", - components: { - ExampleComponent: { - paramA: "example2", - paramB: 15, - }, - }, - }); - - writeFileSync(savePath, JSON.stringify(save, null, 2)); - - const { exitCode } = await runCli(["generate", "-d", appDir]); - expect(exitCode).toBe(0); - - const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8"); - - expect(content).toContain('import { HealthComponent } from "./components/health.component"'); - expect(content).toContain("new HealthComponent(100)"); - - expect(content).toContain("const exampleEntity2 = registry.spawnEntity()"); - expect(content).toContain( - 'registry.addComponent(exampleEntity2, new ExampleComponent("example2", 15, undefined))', - ); - - // Original component should still be present - - expect(content).toContain('import { ExampleComponent } from "./components/example.component"'); - expect(content).toContain("const exampleEntity = registry.spawnEntity()"); - expect(content).toContain( - 'registry.addComponent(exampleEntity, new ExampleComponent("example", 10, undefined))', - ); - }); - - it("should not generate server directory", async () => { - expect(existsSync(resolve(appDir, "server"))).toBe(false); - }); - - it("should generate client editor main.ts", async () => { - await runCli(["generate", "-d", appDir, "--editor"]); - - const content = readFileSync(resolve(appDir, ".nanoforge/editor/client/main.ts"), "utf-8"); - - expect(content).toContain("NanoforgeFactory.createClient()"); - expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"'); - expect(content).toContain('from "@nanoforge-dev/core-editor"'); - }); -}); - -describe("nf generate (TypeScript, with server)", () => { - const projectDir = resolve(tmpDir, "gen-ts-with-server"); - const appDir = resolve(projectDir, "gen-server-app"); - - beforeAll(async () => { - mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "gen-server-app", - "--language", - "ts", - "--package-manager", - "npm", - "--no-strict", - "--server", - "--init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); - }); - - it("should run the generate command with server enabled", async () => { - const { exitCode } = await runCli(["generate", "-d", appDir]); - - expect(exitCode).toBe(0); - }); - - it("should generate server main.ts file", async () => { - await runCli(["generate", "-d", appDir]); - - expect(existsSync(resolve(appDir, "server/main.ts"))).toBe(true); - }); - - it("should generate valid TypeScript server main content", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8"); - - // Core imports - expect(content).toContain('import { type IRunOptions } from "@nanoforge-dev/common"'); - expect(content).toContain('import { NanoforgeFactory } from "@nanoforge-dev/core"'); - - // Server-specific library - expect(content).toContain('import { ECSServerLibrary } from "@nanoforge-dev/ecs-server"'); - - // Factory creates server (not client) - expect(content).toContain("NanoforgeFactory.createServer()"); - expect(content).not.toContain("NanoforgeFactory.createClient()"); - - // Main function signature - expect(content).toContain("export async function main(options: IRunOptions)"); - - // App lifecycle - expect(content).toContain("await app.init(options)"); - expect(content).toContain("await app.run()"); - }); - - it("should generate server init function imports and calls", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8"); - - expect(content).toContain('import { beforeInit } from "./init/before-init"'); - expect(content).toContain('import { afterInit } from "./init/after-init"'); - expect(content).toContain("await beforeInit(app)"); - expect(content).toContain("await afterInit(app)"); - }); - - it("should generate server ECS setup", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8"); - - expect(content).toContain("const ecsLibrary = new ECSServerLibrary()"); - expect(content).toContain("app.useComponentSystem(ecsLibrary)"); - expect(content).toContain("const registry = ecsLibrary.registry"); - expect(content).toContain("const exampleEntity = registry.spawnEntity()"); - expect(content).toContain("registry.addSystem(exampleSystem)"); - }); - - it("should regenerate server main.ts with modified save file (add system)", async () => { - const savePath = resolve(appDir, ".nanoforge/server.save.json"); - const save = JSON.parse(readFileSync(savePath, "utf-8")); - - save.systems.push({ - name: "movementSystem", - path: "./systems/movement.system.ts", - }); - - writeFileSync(savePath, JSON.stringify(save, null, 2)); - - const { exitCode } = await runCli(["generate", "-d", appDir]); - expect(exitCode).toBe(0); - - const content = readFileSync(resolve(appDir, "server/main.ts"), "utf-8"); - - expect(content).toContain('import { movementSystem } from "./systems/movement.system"'); - expect(content).toContain("registry.addSystem(movementSystem)"); - // Original system should still be present - expect(content).toContain('import { exampleSystem } from "./systems/example.system"'); - expect(content).toContain("registry.addSystem(exampleSystem)"); - }); - - it("should also regenerate client main.ts", async () => { - await runCli(["generate", "-d", appDir]); - - const content = readFileSync(resolve(appDir, "client/main.ts"), "utf-8"); - - expect(content).toContain("NanoforgeFactory.createClient()"); - expect(content).toContain('import { ECSClientLibrary } from "@nanoforge-dev/ecs-client"'); - }); -}); - -describe("nf generate (with invalid directory)", () => { - it("should fail when directory does not exist", async () => { - const { exitCode } = await runCli(["generate", "-d", resolve(tmpDir, "nonexistent")]); - - expect(exitCode).not.toBe(0); - }); - - it("should fail when no config file is found", async () => { - const emptyDir = resolve(tmpDir, "empty-dir"); - mkdirSync(emptyDir, { recursive: true }); - - const { exitCode } = await runCli(["generate", "-d", emptyDir]); - - expect(exitCode).not.toBe(0); - }); -}); diff --git a/e2e/cli-help.test.ts b/e2e/cli-help.test.ts index d5a1311..662025d 100644 --- a/e2e/cli-help.test.ts +++ b/e2e/cli-help.test.ts @@ -10,7 +10,6 @@ describe("nf --help", () => { expect(stdout).toContain("Usage: nf [options]"); expect(stdout).toContain("build"); expect(stdout).toContain("dev"); - expect(stdout).toContain("generate"); expect(stdout).toContain("install|add"); expect(stdout).toContain("new"); expect(stdout).toContain("start"); @@ -57,7 +56,6 @@ describe("nf build --help", () => { expect(exitCode).toBe(0); expect(stdout).toContain("build your game"); expect(stdout).toContain("--directory"); - expect(stdout).toContain("--config"); expect(stdout).toContain("--client-entry"); expect(stdout).toContain("--server-entry"); expect(stdout).toContain("--client-static-dir"); @@ -69,18 +67,6 @@ describe("nf build --help", () => { }); }); -describe("nf generate --help", () => { - it("should display generate command options", async () => { - const { stdout, exitCode } = await runCli(["generate", "--help"]); - - expect(exitCode).toBe(0); - expect(stdout).toContain("generate nanoforge files from config"); - expect(stdout).toContain("--directory"); - expect(stdout).toContain("--config"); - expect(stdout).toContain("--watch"); - }); -}); - describe("nf install --help", () => { it("should display install command options", async () => { const { stdout, exitCode } = await runCli(["install", "--help"]); @@ -100,7 +86,6 @@ describe("nf start --help", () => { expect(exitCode).toBe(0); expect(stdout).toContain("start your game"); expect(stdout).toContain("--directory"); - expect(stdout).toContain("--config"); expect(stdout).toContain("--port"); expect(stdout).toContain("--watch"); }); @@ -113,7 +98,6 @@ describe("nf dev --help", () => { expect(exitCode).toBe(0); expect(stdout).toContain("run your game in dev mode"); expect(stdout).toContain("--directory"); - expect(stdout).toContain("--generate"); expect(stdout).toContain("--editor"); }); }); diff --git a/e2e/cli-install.test.ts b/e2e/cli-install.test.ts index 3106204..9bcdeed 100644 --- a/e2e/cli-install.test.ts +++ b/e2e/cli-install.test.ts @@ -1,4 +1,4 @@ -import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { resolve } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; @@ -18,26 +18,14 @@ describe("nf install (with existing project)", () => { const projectDir = resolve(tmpDir, "install-project"); const appDir = resolve(projectDir, "install-app"); - beforeAll(async () => { - mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "install-app", - "--language", - "ts", - "--package-manager", - "npm", - "--strict", - "--no-server", - "--no-init-functions", - "--no-skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); + beforeAll(() => { + // `install -l` only needs a package.json: a generated project cannot be used until the + // engine v2 packages it depends on are published on npm. + mkdirSync(appDir, { recursive: true }); + writeFileSync( + resolve(appDir, "package.json"), + JSON.stringify({ name: "install-app", private: true }, null, 2), + ); }); it("should run the install command with a library name", async () => { @@ -124,8 +112,7 @@ describe("nf install (without library name)", () => { "npm", "--strict", "--no-server", - "--no-init-functions", - "--no-skip-install", + "--skip-install", "--no-docker", "--no-git", "-d", diff --git a/e2e/cli-new-config.test.ts b/e2e/cli-new-config.test.ts index 80ef8eb..72f61e4 100644 --- a/e2e/cli-new-config.test.ts +++ b/e2e/cli-new-config.test.ts @@ -6,6 +6,24 @@ import { runCli } from "./helpers/run-cli"; const tmpDir = resolve(__dirname, "../.tmp-e2e-new-config"); +const newProject = (name: string, directory: string, extraArgs: string[]) => + runCli([ + "new", + "--name", + name, + "--language", + "ts", + "--package-manager", + "npm", + "--strict", + "--skip-install", + "--no-docker", + "--no-git", + ...extraArgs, + "-d", + directory, + ]); + beforeAll(() => { mkdirSync(tmpDir, { recursive: true }); }); @@ -16,79 +34,46 @@ afterAll(() => { describe("nf new config output (no server)", () => { const projectDir = resolve(tmpDir, "config-no-server"); - let config: any; + let config: string; beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "config-test", - "--language", - "ts", - "--package-manager", - "npm", - "--strict", - "--no-server", - "--no-init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); - - config = JSON.parse( - readFileSync(resolve(projectDir, "config-test/nanoforge.config.json"), "utf-8"), - ); + await newProject("config-test", projectDir, ["--no-server"]); + config = readFileSync(resolve(projectDir, "config-test/nanoforge.config.ts"), "utf-8"); }); - it("should have client config", () => { - expect(config.client.enable).toBe(true); + it("should generate a client project config", () => { + expect(config).toContain('type: "client"'); }); - it("should not have server enabled", () => { - expect(config.server?.enable).not.toBe(true); + it("should use defineConfig from nanoforge", () => { + expect(config).toContain('import { defineConfig } from "nanoforge/config";'); }); }); describe("nf new config output (with server)", () => { const projectDir = resolve(tmpDir, "config-with-server"); - let config: any; + const workspaceDir = resolve(projectDir, "server-test"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); + await newProject("server-test", projectDir, ["--server"]); + }); - await runCli([ - "new", - "--name", - "server-test", - "--language", - "ts", - "--package-manager", - "npm", - "--no-strict", - "--server", - "--no-init-functions", - "--skip-install", - "--no-docker", - "--no-git", - "-d", - projectDir, - ]); - - config = JSON.parse( - readFileSync(resolve(projectDir, "server-test/nanoforge.config.json"), "utf-8"), - ); + it("should generate a workspace config including the apps", () => { + const config = readFileSync(resolve(workspaceDir, "nanoforge.config.ts"), "utf-8"); + expect(config).toContain('type: "workspace"'); + expect(config).toContain('packages: ["apps/*"]'); }); - it("should have server enabled", () => { - expect(config.server.enable).toBe(true); + it("should generate a client project config", () => { + const config = readFileSync(resolve(workspaceDir, "apps/client/nanoforge.config.ts"), "utf-8"); + expect(config).toContain('type: "client"'); }); - it("should have client build config", () => { - expect(config.client.enable).toBe(true); + it("should generate a server project config", () => { + const config = readFileSync(resolve(workspaceDir, "apps/server/nanoforge.config.ts"), "utf-8"); + expect(config).toContain('type: "server"'); }); }); @@ -98,26 +83,7 @@ describe("nf new package.json output", () => { beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); - - await runCli([ - "new", - "--name", - "pkg-app", - "--language", - "ts", - "--package-manager", - "npm", - "--strict", - "--no-server", - "--no-init-functions", - "--skip-install", - "--no-docker", - "--editor", - "--no-git", - "-d", - projectDir, - ]); - + await newProject("pkg-app", projectDir, ["--no-server", "--editor"]); pkg = JSON.parse(readFileSync(resolve(projectDir, "pkg-app/package.json"), "utf-8")); }); @@ -127,7 +93,11 @@ describe("nf new package.json output", () => { it("should have nanoforge dependencies", () => { const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; - expect(allDeps).toHaveProperty("@nanoforge-dev/core"); - expect(allDeps).toHaveProperty("@nanoforge-dev/core-editor"); + expect(allDeps).toHaveProperty("nanoforge"); + expect(allDeps).toHaveProperty("@nanoforge-dev/ecs"); + expect(allDeps).toHaveProperty("@nanoforge-dev/graphics-2d"); }); + + // TODO: `--editor` does not add any editor dependency to the generated project yet. + it.todo("should have editor dependencies with --editor"); }); diff --git a/e2e/cli-new.test.ts b/e2e/cli-new.test.ts index 931e601..2eacf09 100644 --- a/e2e/cli-new.test.ts +++ b/e2e/cli-new.test.ts @@ -16,6 +16,7 @@ afterAll(() => { describe("nf new (TypeScript, no server)", () => { const projectDir = resolve(tmpDir, "ts-no-server"); + const appDir = resolve(projectDir, "ts-app"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); @@ -32,8 +33,7 @@ describe("nf new (TypeScript, no server)", () => { "npm", "--strict", "--no-server", - "--no-init-functions", - "--no-skip-install", + "--skip-install", "--no-docker", "--no-git", "-d", @@ -46,47 +46,74 @@ describe("nf new (TypeScript, no server)", () => { }); it("should create the project directory", () => { - expect(existsSync(resolve(projectDir, "ts-app"))).toBe(true); + expect(existsSync(appDir)).toBe(true); }); - it("should generate nanoforge.config.json", () => { - const configPath = resolve(projectDir, "ts-app/nanoforge.config.json"); + it("should generate a client nanoforge.config.ts", () => { + const configPath = resolve(appDir, "nanoforge.config.ts"); expect(existsSync(configPath)).toBe(true); - - const config = JSON.parse(readFileSync(configPath, "utf-8")); - expect(config.client).toBeDefined(); - expect(config.client.enable).toBe(true); + expect(readFileSync(configPath, "utf-8")).toContain('type: "client"'); }); it("should generate package.json", () => { - expect(existsSync(resolve(projectDir, "ts-app/package.json"))).toBe(true); + const pkg = JSON.parse(readFileSync(resolve(appDir, "package.json"), "utf-8")); + expect(pkg.name).toBe("ts-app"); }); it("should generate tsconfig.json for TypeScript", () => { - expect(existsSync(resolve(projectDir, "ts-app/tsconfig.json"))).toBe(true); + expect(existsSync(resolve(appDir, "tsconfig.json"))).toBe(true); }); - it("should generate client directory", () => { - expect(existsSync(resolve(projectDir, "ts-app/client"))).toBe(true); + it("should generate the client entry file", () => { + expect(existsSync(resolve(appDir, "src/main.ts"))).toBe(true); }); - it("should generate client main file", () => { - expect(existsSync(resolve(projectDir, "ts-app/client/main.ts"))).toBe(true); + it("should generate components and systems", () => { + expect(existsSync(resolve(appDir, "src/components"))).toBe(true); + expect(existsSync(resolve(appDir, "src/systems"))).toBe(true); }); - it("should not generate server directory", () => { - expect(existsSync(resolve(projectDir, "ts-app/server"))).toBe(false); + it("should not generate a workspace", () => { + expect(existsSync(resolve(appDir, "apps"))).toBe(false); }); - it("should not have server enabled in config", () => { - const configPath = resolve(projectDir, "ts-app/nanoforge.config.json"); - const config = JSON.parse(readFileSync(configPath, "utf-8")); - expect(config.server?.enable).not.toBe(true); + it("should not generate docker files", () => { + expect(existsSync(resolve(appDir, "Dockerfile"))).toBe(false); + }); +}); + +describe("nf new (with dependencies installation)", () => { + // TODO: enable once the engine v2 packages (`@nanoforge-dev/ecs`, `@nanoforge-dev/network` + // and `nanoforge`) are published on npm, installing a generated project fails until then. + it.skip("should install the project dependencies", async () => { + const projectDir = resolve(tmpDir, "with-install"); + mkdirSync(projectDir, { recursive: true }); + + const { exitCode } = await runCli([ + "new", + "--name", + "install-app", + "--language", + "ts", + "--package-manager", + "npm", + "--strict", + "--no-server", + "--no-skip-install", + "--no-docker", + "--no-git", + "-d", + projectDir, + ]); + + expect(exitCode).toBe(0); + expect(existsSync(resolve(projectDir, "install-app/node_modules"))).toBe(true); }); }); describe("nf new (JavaScript, with server)", () => { const projectDir = resolve(tmpDir, "js-with-server"); + const workspaceDir = resolve(projectDir, "js-game"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); @@ -103,8 +130,7 @@ describe("nf new (JavaScript, with server)", () => { "pnpm", "--no-strict", "--server", - "--init-functions", - "--no-skip-install", + "--skip-install", "--no-docker", "--no-git", "-d", @@ -115,42 +141,33 @@ describe("nf new (JavaScript, with server)", () => { expect(stdout).toContain("Project successfully created"); }); - it("should generate server directory", () => { - expect(existsSync(resolve(projectDir, "js-game/server"))).toBe(true); + it("should generate a workspace nanoforge.config.js", () => { + const content = readFileSync(resolve(workspaceDir, "nanoforge.config.js"), "utf-8"); + expect(content).toContain('type: "workspace"'); + expect(content).toContain('packages: ["apps/*"]'); }); - it("should generate server main file as .js", () => { - expect(existsSync(resolve(projectDir, "js-game/server/main.js"))).toBe(true); + it("should generate a pnpm workspace", () => { + expect(existsSync(resolve(workspaceDir, "pnpm-workspace.yaml"))).toBe(true); }); - it("should generate client main file as .js", () => { - expect(existsSync(resolve(projectDir, "js-game/client/main.js"))).toBe(true); + it("should generate the client project", () => { + const content = readFileSync(resolve(workspaceDir, "apps/client/nanoforge.config.js"), "utf-8"); + expect(content).toContain('type: "client"'); + expect(existsSync(resolve(workspaceDir, "apps/client/src/main.js"))).toBe(true); }); - it("should have server enabled in config", () => { - const configPath = resolve(projectDir, "js-game/nanoforge.config.json"); - const config = JSON.parse(readFileSync(configPath, "utf-8")); - expect(config.server.enable).toBe(true); - }); - - it("should generate init function files for client", () => { - expect(existsSync(resolve(projectDir, "js-game/client/init/before-init.js"))).toBe(true); - expect(existsSync(resolve(projectDir, "js-game/client/init/after-init.js"))).toBe(true); - }); - - it("should generate init function files for server", () => { - expect(existsSync(resolve(projectDir, "js-game/server/init/before-init.js"))).toBe(true); - expect(existsSync(resolve(projectDir, "js-game/server/init/after-init.js"))).toBe(true); + it("should generate the server project", () => { + const content = readFileSync(resolve(workspaceDir, "apps/server/nanoforge.config.js"), "utf-8"); + expect(content).toContain('type: "server"'); + expect(existsSync(resolve(workspaceDir, "apps/server/src/main.js"))).toBe(true); }); it("should generate jsconfig.json instead of tsconfig.json", () => { - expect(existsSync(resolve(projectDir, "js-game/jsconfig.json"))).toBe(true); - expect(existsSync(resolve(projectDir, "js-game/tsconfig.json"))).toBe(false); - }); - - it("should generate save files in .nanoforge", () => { - expect(existsSync(resolve(projectDir, "js-game/.nanoforge/client.save.json"))).toBe(true); - expect(existsSync(resolve(projectDir, "js-game/.nanoforge/server.save.json"))).toBe(true); + expect(existsSync(resolve(workspaceDir, "jsconfig.json"))).toBe(true); + expect(existsSync(resolve(workspaceDir, "tsconfig.json"))).toBe(false); + expect(existsSync(resolve(workspaceDir, "apps/client/jsconfig.json"))).toBe(true); + expect(existsSync(resolve(workspaceDir, "apps/server/jsconfig.json"))).toBe(true); }); }); @@ -174,7 +191,6 @@ describe("nf new (with --path option)", () => { "npm", "--strict", "--no-server", - "--no-init-functions", "--skip-install", "--no-docker", "--no-git", @@ -187,12 +203,14 @@ describe("nf new (with --path option)", () => { }); it("should create the project in the custom path", () => { - expect(existsSync(resolve(projectDir, "custom/subdir"))).toBe(true); + expect(existsSync(resolve(projectDir, "custom/subdir/nanoforge.config.ts"))).toBe(true); + expect(existsSync(resolve(projectDir, "path-app"))).toBe(false); }); }); describe("nf new (with typescript with docker option)", () => { const projectDir = resolve(tmpDir, "ts-with-docker"); + const appDir = resolve(projectDir, "ts-app"); beforeAll(async () => { mkdirSync(projectDir, { recursive: true }); @@ -209,8 +227,7 @@ describe("nf new (with typescript with docker option)", () => { "npm", "--strict", "--no-server", - "--no-init-functions", - "--no-skip-install", + "--skip-install", "--docker", "--no-git", "-d", @@ -222,50 +239,16 @@ describe("nf new (with typescript with docker option)", () => { expect(stdout).toContain("Project successfully created"); }); - it("should create the project directory", () => { - expect(existsSync(resolve(projectDir, "ts-app"))).toBe(true); - }); - - it("should generate nanoforge.config.json", () => { - const configPath = resolve(projectDir, "ts-app/nanoforge.config.json"); - expect(existsSync(configPath)).toBe(true); - - const config = JSON.parse(readFileSync(configPath, "utf-8")); - expect(config.client).toBeDefined(); - expect(config.client.enable).toBe(true); - }); - - it("should generate package.json", () => { - expect(existsSync(resolve(projectDir, "ts-app/package.json"))).toBe(true); - }); - - it("should generate tsconfig.json for TypeScript", () => { - expect(existsSync(resolve(projectDir, "ts-app/tsconfig.json"))).toBe(true); - }); - - it("should generate client directory", () => { - expect(existsSync(resolve(projectDir, "ts-app/client"))).toBe(true); - }); - - it("should generate client main file", () => { - expect(existsSync(resolve(projectDir, "ts-app/client/main.ts"))).toBe(true); - }); - - it("should not generate server directory", () => { - expect(existsSync(resolve(projectDir, "ts-app/server"))).toBe(false); - }); - - it("should not have server enabled in config", () => { - const configPath = resolve(projectDir, "ts-app/nanoforge.config.json"); - const config = JSON.parse(readFileSync(configPath, "utf-8")); - expect(config.server?.enable).not.toBe(true); + it("should generate the project", () => { + expect(existsSync(resolve(appDir, "nanoforge.config.ts"))).toBe(true); + expect(existsSync(resolve(appDir, "src/main.ts"))).toBe(true); }); it("should generate Dockerfile", () => { - expect(existsSync(resolve(projectDir, "ts-app/Dockerfile"))).toBe(true); + expect(existsSync(resolve(appDir, "Dockerfile"))).toBe(true); }); it("should generate .dockerignore", () => { - expect(existsSync(resolve(projectDir, "ts-app/.dockerignore"))).toBe(true); + expect(existsSync(resolve(appDir, ".dockerignore"))).toBe(true); }); }); diff --git a/e2e/helpers/project-fixtures.ts b/e2e/helpers/project-fixtures.ts new file mode 100644 index 0000000..d44656d --- /dev/null +++ b/e2e/helpers/project-fixtures.ts @@ -0,0 +1,36 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const CONFIG_IMPORT = 'import { defineConfig } from "nanoforge/config";\n'; +const DEFINE_CONFIG_CALL = /defineConfig\(([\s\S]*)\);/; + +/** + * Configs generated by `nf new` import `defineConfig` from `nanoforge/config`, which is not + * published yet, so the CLI cannot load them in a project scaffolded with `--skip-install`. + * `defineConfig` is an identity function: stripping the import and the call keeps the exact + * generated config while making it loadable without dependencies. + * + * TODO: remove once `nanoforge` is published with its `./config` export. + */ +export const stripDefineConfig = (projectDir: string): void => { + const path = ["nanoforge.config.ts", "nanoforge.config.js"] + .map((file) => join(projectDir, file)) + .find((file) => existsSync(file)); + if (!path) throw new Error(`No nanoforge config found in '${projectDir}'`); + + const content = readFileSync(path, "utf-8"); + if (!content.includes(CONFIG_IMPORT) || !DEFINE_CONFIG_CALL.test(content)) + throw new Error(`Unexpected config format in '${path}', update stripDefineConfig`); + + writeFileSync(path, content.replace(CONFIG_IMPORT, "").replace(DEFINE_CONFIG_CALL, "$1;")); +}; + +/** + * Engine packages imported by the generated entry file are not published yet, so builds run + * against an entry file without any dependency. + * + * TODO: remove once the engine v2 packages are published. + */ +export const writeStandaloneEntry = (projectDir: string, entry = "src/main.ts"): void => { + writeFileSync(join(projectDir, entry), 'console.log("nanoforge");\n'); +}; diff --git a/eslint.config.js b/eslint.config.js index b0da256..0f0f63c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,3 +1,8 @@ import eslintConfig from "@nanoforge-dev/utils-eslint-config"; -export default eslintConfig; +export default [ + ...eslintConfig, + { + ignores: ["libs/**"], + }, +]; diff --git a/libs/config/.cliff-jumperrc.json b/libs/config/.cliff-jumperrc.json new file mode 100644 index 0000000..ae950d4 --- /dev/null +++ b/libs/config/.cliff-jumperrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", + "name": "config", + "org": "nanoforge-dev", + "packagePath": "libs/config", + "tagTemplate": "{{new-version}}", + "identifierBase": false +} diff --git a/libs/config/.gitignore b/libs/config/.gitignore new file mode 100644 index 0000000..88ef62e --- /dev/null +++ b/libs/config/.gitignore @@ -0,0 +1,234 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Turbo +.turbo/ + +# Compiled files +src/**/*.js +src/**/*.d.ts + +# mint-tsdocs cache +docs/.tsdocs/ diff --git a/libs/config/.prettierignore b/libs/config/.prettierignore new file mode 100644 index 0000000..64b127c --- /dev/null +++ b/libs/config/.prettierignore @@ -0,0 +1,11 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock +bun.lock + +.turbo/ +node_modules/ +dist/ +coverage/ +CHANGELOG.md diff --git a/libs/config/CHANGELOG.md b/libs/config/CHANGELOG.md new file mode 100644 index 0000000..6361e43 --- /dev/null +++ b/libs/config/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to this project will be documented in this file. diff --git a/libs/config/LICENSE b/libs/config/LICENSE new file mode 100644 index 0000000..4672486 --- /dev/null +++ b/libs/config/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2026 NanoForge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/config/README.md b/libs/config/README.md new file mode 100644 index 0000000..05cc5c3 --- /dev/null +++ b/libs/config/README.md @@ -0,0 +1,108 @@ +
+
+

+ NanoForge +

+
+

+ npm version + npm downloads + Tests status + Release status + Last commit + Contributors +

+
+ +## About + +`@nanoforge-dev/config` is a library with the config types and helpers used to define and resolve `nanoforge.config.ts` files across Nanoforge CLI. + +## Installation + +**Node.js 26 or newer is required.** + +```sh +npm install @nanoforge-dev/config +yarn add @nanoforge-dev/config +pnpm add @nanoforge-dev/config +bun add @nanoforge-dev/config +``` + +## Usage + +Wrap a `nanoforge.config.ts` file's default export in `defineConfig` to get type-checking and editor autocompletion: + +```ts +import { defineConfig } from "@nanoforge-dev/config"; + +export default defineConfig({ + type: "client", + entryFile: "src/main.ts", + libs: ["../../libs/my-lib"], +}); +``` + +The `type` field discriminates between the four config shapes: + +- `workspace` — a monorepo root listing its `packages`. +- `lib` — a shared library consumed by clients/servers. +- `client` — a browser-side NanoForge app. +- `server` — a server-side NanoForge app. + +Each config type has partial, optional fields. `resolveConfig` (and its per-type variants `resolveWorkspaceConfig`, `resolveLibConfig`, `resolveClientConfig`, `resolveServerConfig`) fill in missing fields with defaults, deep-merging nested objects like `dir` and `out`: + +```ts +import { resolveConfig } from "@nanoforge-dev/config"; + +const resolved = resolveConfig({ type: "client", dir: { assets: "static" } }); +// resolved.dir.assets === "static" +// resolved.dir.packages === "nf_modules" (default, not overridden) +``` + +Array fields (`packages`, `libs`) are replaced wholesale by an override rather than concatenated with the defaults. + +### Loading a config file + +`parseConfig` loads a `nanoforge.config.ts`/`.js` file from a given path, validates its default export, and resolves it against the defaults in one step: + +```ts +import { parseConfig } from "@nanoforge-dev/config"; + +const config = await parseConfig("./nanoforge.config.ts"); +``` + +If the file is missing, fails to load, has no default export, or its default export isn't a recognizable `NanoforgeConfig`, `parseConfig` rejects with a `ConfigParseError` whose `code` (`"not-found" | "load-failed" | "no-default-export" | "invalid-type"`) identifies the failure: + +```ts +import { ConfigParseError, parseConfig } from "@nanoforge-dev/config"; + +try { + await parseConfig("./nanoforge.config.ts"); +} catch (error) { + if (error instanceof ConfigParseError && error.code === "not-found") { + // ... + } +} +``` + +## Links + +- [GitHub][source] +- [npm][npm] + +## Contributing + +Before creating an issue, please ensure that it hasn't already been reported/suggested, and double-check the +[documentation][documentation]. +See [the contribution guide][contributing] if you'd like to submit a PR. + +## Help + +If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please don't hesitate to ask questions in [discussions][discussions]. + +[documentation]: https://github.com/NanoForge-dev/CLI +[discussions]: https://github.com/NanoForge-dev/CLI/discussions +[source]: https://github.com/NanoForge-dev/CLI/tree/main/libs/config +[npm]: https://www.npmjs.com/package/@nanoforge-dev/config +[contributing]: https://github.com/NanoForge-dev/CLI/blob/main/.github/CONTRIBUTING.md diff --git a/libs/config/cliff.toml b/libs/config/cliff.toml new file mode 100644 index 0000000..4bb6a65 --- /dev/null +++ b/libs/config/cliff.toml @@ -0,0 +1,79 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file.\n +""" +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +{% if version %}\ + # [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + ({{ self::remote_url() }}/tree/{{ version }})\ + {% endif %}\ + {% endif %} \ + - ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %}\ + # [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ + {% if commit.breaking %}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + - @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^docs", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^refactor", group = "Refactor"}, + { message = "^types", group = "Typings"}, + { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"}, + { message = "^revert", skip = true}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^chore", skip = true}, + { message = "^ci", skip = true}, + { message = "^build", skip = true}, + { body = ".*security", group = "Security"}, +] +filter_commits = true +protect_breaking_commits = true +tag_pattern = "^[0-9]+" +ignore_tags = "" +topo_order = false +sort_commits = "newest" + +[remote.github] +owner = "NanoForge-dev" +repo = "CLI" diff --git a/libs/config/eslint.config.js b/libs/config/eslint.config.js new file mode 100644 index 0000000..62ec06d --- /dev/null +++ b/libs/config/eslint.config.js @@ -0,0 +1,3 @@ +import config from "@nanoforge-dev/utils-eslint-config"; + +export default config; diff --git a/libs/config/package.json b/libs/config/package.json new file mode 100644 index 0000000..66a0346 --- /dev/null +++ b/libs/config/package.json @@ -0,0 +1,90 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@nanoforge-dev/config", + "version": "1.4.2", + "description": "NanoForge CLI - Config", + "keywords": [ + "nanoforge", + "game", + "engine" + ], + "homepage": "https://github.com/NanoForge-dev/CLI#readme", + "bugs": "https://github.com/NanoForge-dev/CLI/issues", + "license": "MIT", + "contributors": [ + "Exelo ", + "Fexkoser ", + "Harper ", + "Josephine ", + "Leo " + ], + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json" + }, + "type": "module", + "directories": { + "lib": "src" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NanoForge-dev/CLI.git", + "directory": "libs/config" + }, + "funding": "https://github.com/NanoForge-dev/CLI?sponsor", + "scripts": { + "build": "tsc --noEmit && tsdown --config-loader unrun", + "lint": "prettier --check . && eslint --format=pretty src", + "format": "prettier --write . && eslint --fix --format=pretty src", + "test:unit": "vitest run --config vitest.config.ts", + "prepack": "pnpm run build && pnpm run lint", + "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'libs/config/*'", + "release": "cliff-jumper", + "docs": "mint-tsdocs generate" + }, + "dependencies": { + "unrun": "catalog:libs", + "zod": "catalog:libs" + }, + "devDependencies": { + "@favware/cliff-jumper": "catalog:ci", + "@nanoforge-dev/utils-eslint-config": "catalog:lint", + "@nanoforge-dev/utils-prettier-config": "catalog:lint", + "@trivago/prettier-plugin-sort-imports": "catalog:lint", + "eslint": "catalog:lint", + "prettier": "catalog:lint", + "tsdown": "catalog:build", + "typescript": "catalog:build", + "vitest": "catalog:test" + }, + "packageManager": "pnpm@12.0.0", + "engines": { + "node": "26" + }, + "publishConfig": { + "access": "public" + }, + "lint-staged": { + "**": [ + "prettier --ignore-unknown --write" + ], + "src/**/*.ts": [ + "eslint --fix" + ] + } +} diff --git a/libs/config/prettier.config.js b/libs/config/prettier.config.js new file mode 100644 index 0000000..27d0e26 --- /dev/null +++ b/libs/config/prettier.config.js @@ -0,0 +1,3 @@ +import config from "@nanoforge-dev/utils-prettier-config"; + +export default config; diff --git a/libs/config/src/defaults/default-configs.ts b/libs/config/src/defaults/default-configs.ts new file mode 100644 index 0000000..2b06faf --- /dev/null +++ b/libs/config/src/defaults/default-configs.ts @@ -0,0 +1,72 @@ +import type { ClientConfig, LibConfig, ServerConfig, WorkspaceConfig } from "../types"; +import type { DeepRequired } from "../types/utils.type"; + +export const defaultWorkspaceConfig: DeepRequired> = { + packages: [], +}; + +export const defaultLibConfig: DeepRequired> = { + dir: { + assets: "assets", + shared: "shared", + + components: "shared/components", + systems: "shared/systems", + scenes: "shared/scenes", + }, +}; + +export const defaultClientConfig: DeepRequired> = { + entryFile: "src/main.ts", + out: { + dir: "dist", + mainFile: "main.js", + }, + + dir: { + assets: "assets", + packages: "nf_modules", + + components: "src/components", + systems: "src/systems", + scenes: "src/scenes", + }, + + editor: { + entryFile: "src/main.ts", + }, + + language: "ts", + + port: "3000", + tls: { + enable: false, + }, + + libs: [], +}; + +export const defaultServerConfig: DeepRequired> = { + entryFile: "src/main.ts", + out: { + dir: "dist", + mainFile: "main.js", + }, + + dir: { + assets: "assets", + packages: "nf_modules", + + components: "src/components", + systems: "src/systems", + scenes: "src/scenes", + }, + + editor: { + entryFile: "src/main.ts", + }, + + language: "ts", + + libs: [], +}; diff --git a/libs/config/src/define-config.ts b/libs/config/src/define-config.ts new file mode 100644 index 0000000..788e025 --- /dev/null +++ b/libs/config/src/define-config.ts @@ -0,0 +1,7 @@ +import type { NanoforgeConfig } from "./types"; + +/** + * Identity function used to get type-checking and editor autocompletion on + * a `nanoforge.config.ts` file's default export. + */ +export const defineConfig = (config: T): T => config; diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts new file mode 100644 index 0000000..efce8b3 --- /dev/null +++ b/libs/config/src/index.ts @@ -0,0 +1,31 @@ +export { + defaultClientConfig, + defaultLibConfig, + defaultServerConfig, + defaultWorkspaceConfig, +} from "./defaults/default-configs"; +export { defineConfig } from "./define-config"; +export { ConfigParseError } from "./parse/config-error"; +export type { ConfigParseErrorCode } from "./parse/config-error"; +export { parseConfig } from "./parse/parse-config"; +export { + resolveClientConfig, + resolveConfig, + resolveLibConfig, + resolveServerConfig, + resolveWorkspaceConfig, +} from "./merge/resolve-config"; +export type { + BaseConfig, + BuildableConfig, + ClientConfig, + ContainLibConfig, + EditorConfig, + LanguageConfig, + LibConfig, + NanoforgeConfig, + ServerConfig, + SourceableConfig, + TlsConfig, + WorkspaceConfig, +} from "./types"; diff --git a/libs/config/src/merge/deep-merge.ts b/libs/config/src/merge/deep-merge.ts new file mode 100644 index 0000000..0d8d912 --- /dev/null +++ b/libs/config/src/merge/deep-merge.ts @@ -0,0 +1,37 @@ +import type { DeepPartial } from "../types/utils.type"; + +const isPlainObject = (value: unknown): value is Record => + typeof value === "object" && value !== null && !Array.isArray(value); + +const mergeUnknown = ( + defaults: Record, + override: Record, +): Record => { + const result: Record = { ...defaults }; + + for (const key of Object.keys(override)) { + const overrideValue = override[key]; + if (overrideValue === undefined) continue; + + const defaultValue = defaults[key]; + result[key] = + isPlainObject(defaultValue) && isPlainObject(overrideValue) + ? mergeUnknown(defaultValue, overrideValue) + : overrideValue; + } + + return result; +}; + +/** + * Recursively merges `override` onto `defaults`. + * + * @remarks + * Nested plain objects (e.g. `dir`, `out`) are merged key by key. Array + * values (e.g. `packages`, `libs`) are replaced wholesale by `override`, + * not concatenated. + */ +export const deepMerge = >( + defaults: T, + override: DeepPartial, +): T => mergeUnknown(defaults, override as Record) as T; diff --git a/libs/config/src/merge/resolve-config.ts b/libs/config/src/merge/resolve-config.ts new file mode 100644 index 0000000..6440b3b --- /dev/null +++ b/libs/config/src/merge/resolve-config.ts @@ -0,0 +1,52 @@ +import { + defaultClientConfig, + defaultLibConfig, + defaultServerConfig, + defaultWorkspaceConfig, +} from "../defaults/default-configs"; +import type { + ClientConfig, + LibConfig, + NanoforgeConfig, + ServerConfig, + WorkspaceConfig, +} from "../types"; +import type { DeepRequired } from "../types/utils.type"; +import { deepMerge } from "./deep-merge"; + +export const resolveWorkspaceConfig = (config: WorkspaceConfig): DeepRequired => ({ + ...deepMerge(defaultWorkspaceConfig, config), + type: "workspace", +}); + +export const resolveLibConfig = (config: LibConfig): DeepRequired => ({ + ...deepMerge(defaultLibConfig, config), + type: "lib", +}); + +export const resolveClientConfig = (config: ClientConfig): DeepRequired => ({ + ...deepMerge(defaultClientConfig, config), + type: "client", +}); + +export const resolveServerConfig = (config: ServerConfig): DeepRequired => ({ + ...deepMerge(defaultServerConfig, config), + type: "server", +}); + +/** + * Merges a `nanoforge.config.ts` config against the defaults matching its + * `type`. + */ +export const resolveConfig = (config: NanoforgeConfig): DeepRequired => { + switch (config.type) { + case "workspace": + return resolveWorkspaceConfig(config); + case "lib": + return resolveLibConfig(config); + case "client": + return resolveClientConfig(config); + case "server": + return resolveServerConfig(config); + } +}; diff --git a/libs/config/src/parse/config-error.ts b/libs/config/src/parse/config-error.ts new file mode 100644 index 0000000..54736ca --- /dev/null +++ b/libs/config/src/parse/config-error.ts @@ -0,0 +1,14 @@ +export type ConfigParseErrorCode = + "not-found" | "load-failed" | "no-default-export" | "invalid-type"; + +export class ConfigParseError extends Error { + constructor( + public readonly code: ConfigParseErrorCode, + message: string, + public readonly path: string, + options?: { cause?: unknown }, + ) { + super(`${message}${options?.cause ? `\n${options?.cause}` : ""}`); + this.name = "ConfigParseError"; + } +} diff --git a/libs/config/src/parse/load-config-module.ts b/libs/config/src/parse/load-config-module.ts new file mode 100644 index 0000000..ae1264d --- /dev/null +++ b/libs/config/src/parse/load-config-module.ts @@ -0,0 +1,38 @@ +import { access } from "node:fs/promises"; + +import { ConfigParseError } from "./config-error"; + +export const loadConfigModule = async (path: string | URL): Promise => { + const pathLabel = String(path); + + try { + await access(path); + } catch (cause) { + throw new ConfigParseError("not-found", `Config file not found: ${pathLabel}`, pathLabel, { + cause, + }); + } + + let module: { default?: unknown }; + try { + const { unrun } = await import("unrun"); + ({ module } = await unrun<{ default?: unknown }>({ path, preset: "bundle-require" })); + } catch (cause) { + throw new ConfigParseError( + "load-failed", + `Failed to load config file: ${pathLabel}`, + pathLabel, + { cause }, + ); + } + + if (module.default === undefined) { + throw new ConfigParseError( + "no-default-export", + `Config file has no default export: ${pathLabel}`, + pathLabel, + ); + } + + return module.default; +}; diff --git a/libs/config/src/parse/parse-config.ts b/libs/config/src/parse/parse-config.ts new file mode 100644 index 0000000..466490e --- /dev/null +++ b/libs/config/src/parse/parse-config.ts @@ -0,0 +1,11 @@ +import { resolveConfig } from "../merge/resolve-config"; +import type { NanoforgeConfig } from "../types"; +import type { DeepRequired } from "../types/utils.type"; +import { loadConfigModule } from "./load-config-module"; +import { assertNanoforgeConfig } from "./validate-config"; + +export const parseConfig = async (path: string | URL): Promise> => { + const raw = await loadConfigModule(path); + assertNanoforgeConfig(raw, String(path)); + return resolveConfig(raw); +}; diff --git a/libs/config/src/parse/validate-config.ts b/libs/config/src/parse/validate-config.ts new file mode 100644 index 0000000..8a07e97 --- /dev/null +++ b/libs/config/src/parse/validate-config.ts @@ -0,0 +1,112 @@ +import { z } from "zod"; + +import type { NanoforgeConfig } from "../types"; +import { ConfigParseError } from "./config-error"; + +const buildableConfigShape = { + entryFile: z.string().optional(), + out: z + .object({ + dir: z.string().optional(), + mainFile: z.string().optional(), + }) + .optional(), +}; + +const containLibConfigShape = { + libs: z.array(z.string()).optional(), +}; + +const editorConfigShape = { + editor: z + .object({ + entryFile: z.string().optional(), + }) + .optional(), +}; + +const languageConfigShape = { + language: z.enum(["ts", "js"]).optional(), +}; + +const tlsConfigShape = { + tls: z + .union([ + z.object({ enable: z.literal(false).optional() }), + z.object({ enable: z.literal(true), cert: z.string(), key: z.string() }), + ]) + .optional(), +}; + +const sourceableConfigShape = { + dir: z + .object({ + assets: z.string().optional(), + packages: z.string().optional(), + components: z.string().optional(), + systems: z.string().optional(), + scenes: z.string().optional(), + }) + .optional(), +}; + +const workspaceConfigSchema = z.object({ + type: z.literal("workspace"), + packages: z.array(z.string()).optional(), +}); + +const libConfigSchema = z.object({ + type: z.literal("lib"), + dir: z + .object({ + assets: z.string().optional(), + shared: z.string().optional(), + components: z.string().optional(), + systems: z.string().optional(), + scenes: z.string().optional(), + }) + .optional(), +}); + +const clientConfigSchema = z.object({ + type: z.literal("client"), + ...sourceableConfigShape, + ...buildableConfigShape, + ...containLibConfigShape, + ...editorConfigShape, + ...languageConfigShape, + ...tlsConfigShape, + port: z.string().optional(), +}); + +const serverConfigSchema = z.object({ + type: z.literal("server"), + ...sourceableConfigShape, + ...buildableConfigShape, + ...containLibConfigShape, + ...editorConfigShape, + ...languageConfigShape, +}); + +const nanoforgeConfigSchema = z.discriminatedUnion("type", [ + workspaceConfigSchema, + libConfigSchema, + clientConfigSchema, + serverConfigSchema, +]); + +export function assertNanoforgeConfig( + value: unknown, + path: string, +): asserts value is NanoforgeConfig { + const result = nanoforgeConfigSchema.safeParse(value); + + if (!result.success) { + throw new ConfigParseError( + "invalid-type", + `Config file's default export is not a valid NanoforgeConfig: ${path}\n${z.prettifyError(result.error)}`, + path, + { cause: result.error }, + ); + } +} diff --git a/libs/config/src/types/base-config.type.ts b/libs/config/src/types/base-config.type.ts new file mode 100644 index 0000000..2cbabf6 --- /dev/null +++ b/libs/config/src/types/base-config.type.ts @@ -0,0 +1,6 @@ +/** + * Common shape every `nanoforge.config.ts` file's default export satisfies. + */ +export interface BaseConfig { + type: T; +} diff --git a/libs/config/src/types/client-config.type.ts b/libs/config/src/types/client-config.type.ts new file mode 100644 index 0000000..b5b1fca --- /dev/null +++ b/libs/config/src/types/client-config.type.ts @@ -0,0 +1,22 @@ +import type { BaseConfig } from "./base-config.type"; +import type { + BuildableConfig, + ContainLibConfig, + EditorConfig, + LanguageConfig, + SourceableConfig, + TlsConfig, +} from "./mixins.type"; + +export interface ClientConfig + extends + BaseConfig<"client">, + SourceableConfig, + BuildableConfig, + ContainLibConfig, + EditorConfig, + LanguageConfig, + TlsConfig { + /** Port the client dev-server loader listens on. */ + port?: string; +} diff --git a/libs/config/src/types/index.ts b/libs/config/src/types/index.ts new file mode 100644 index 0000000..3a0523a --- /dev/null +++ b/libs/config/src/types/index.ts @@ -0,0 +1,21 @@ +import type { ClientConfig } from "./client-config.type"; +import type { LibConfig } from "./lib-config.type"; +import type { ServerConfig } from "./server-config.type"; +import type { WorkspaceConfig } from "./workspace-config.type"; + +export type { BaseConfig } from "./base-config.type"; +export type { ClientConfig } from "./client-config.type"; +export type { LibConfig } from "./lib-config.type"; +export type { + BuildableConfig, + ContainLibConfig, + EditorConfig, + LanguageConfig, + SourceableConfig, + TlsConfig, +} from "./mixins.type"; +export type { ServerConfig } from "./server-config.type"; +export type { WorkspaceConfig } from "./workspace-config.type"; + +/** Discriminated union of every `nanoforge.config.ts` shape, keyed by `type`. */ +export type NanoforgeConfig = WorkspaceConfig | LibConfig | ClientConfig | ServerConfig; diff --git a/libs/config/src/types/lib-config.type.ts b/libs/config/src/types/lib-config.type.ts new file mode 100644 index 0000000..3b2c3cb --- /dev/null +++ b/libs/config/src/types/lib-config.type.ts @@ -0,0 +1,15 @@ +import type { BaseConfig } from "./base-config.type"; + +export interface LibConfig extends BaseConfig<"lib"> { + dir?: { + assets?: string; + shared?: string; + + /** Used by the editor only. Should be inside `shared`. */ + components?: string; + /** Used by the editor only. Should be inside `shared`. */ + systems?: string; + /** Used by the editor only. Should be inside `shared`. */ + scenes?: string; + }; +} diff --git a/libs/config/src/types/mixins.type.ts b/libs/config/src/types/mixins.type.ts new file mode 100644 index 0000000..50785f9 --- /dev/null +++ b/libs/config/src/types/mixins.type.ts @@ -0,0 +1,49 @@ +export interface ContainLibConfig { + libs?: string[]; +} + +export interface BuildableConfig { + entryFile?: string; + out?: { + dir?: string; + mainFile?: string; + }; +} + +export interface EditorConfig { + editor?: { + /** Entry point used to build/run this project in editor mode (`--editor`). */ + entryFile?: string; + }; +} + +export interface LanguageConfig { + /** Source language, used to pick `.ts`/`.js` component and system schematic templates. */ + language?: "ts" | "js"; +} + +export interface TlsConfig { + tls?: + | { + enable?: false; + } + | { + enable: true; + cert: string; + key: string; + }; +} + +export interface SourceableConfig { + dir?: { + assets?: string; + packages?: string; + + /** Used by the editor only. */ + components?: string; + /** Used by the editor only. */ + systems?: string; + /** Used by the editor only. */ + scenes?: string; + }; +} diff --git a/libs/config/src/types/server-config.type.ts b/libs/config/src/types/server-config.type.ts new file mode 100644 index 0000000..c7f3abc --- /dev/null +++ b/libs/config/src/types/server-config.type.ts @@ -0,0 +1,17 @@ +import type { BaseConfig } from "./base-config.type"; +import type { + BuildableConfig, + ContainLibConfig, + EditorConfig, + LanguageConfig, + SourceableConfig, +} from "./mixins.type"; + +export interface ServerConfig + extends + BaseConfig<"server">, + SourceableConfig, + BuildableConfig, + ContainLibConfig, + EditorConfig, + LanguageConfig {} diff --git a/libs/config/src/types/utils.type.ts b/libs/config/src/types/utils.type.ts new file mode 100644 index 0000000..bc71828 --- /dev/null +++ b/libs/config/src/types/utils.type.ts @@ -0,0 +1,3 @@ +export type DeepRequired = T extends object ? { [P in keyof T]-?: DeepRequired } : T; + +export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; diff --git a/libs/config/src/types/workspace-config.type.ts b/libs/config/src/types/workspace-config.type.ts new file mode 100644 index 0000000..8d45c7d --- /dev/null +++ b/libs/config/src/types/workspace-config.type.ts @@ -0,0 +1,5 @@ +import type { BaseConfig } from "./base-config.type"; + +export interface WorkspaceConfig extends BaseConfig<"workspace"> { + packages?: string[]; +} diff --git a/libs/config/test/define-config.spec.ts b/libs/config/test/define-config.spec.ts new file mode 100644 index 0000000..0bd4071 --- /dev/null +++ b/libs/config/test/define-config.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; + +import { defineConfig } from "../src"; +import type { NanoforgeConfig } from "../src"; + +describe("defineConfig", () => { + it("returns the given config unchanged", () => { + const config = defineConfig({ type: "client", libs: ["../libs/test-lib"] }); + expect(config).toEqual({ type: "client", libs: ["../libs/test-lib"] }); + }); + + it("narrows to the matching config shape via the type discriminant", () => { + const config: NanoforgeConfig = defineConfig({ type: "server" }); + if (config.type === "server") { + expect(config.libs).toBeUndefined(); + } else { + throw new Error("expected type to be 'server'"); + } + }); +}); diff --git a/libs/config/test/parse-config.spec.ts b/libs/config/test/parse-config.spec.ts new file mode 100644 index 0000000..2d98c72 --- /dev/null +++ b/libs/config/test/parse-config.spec.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; + +import { ConfigParseError, parseConfig } from "../src"; + +const fixture = (name: string) => new URL(`./parse/fixtures/${name}`, import.meta.url); + +describe("parseConfig", () => { + it("resolves a .ts config file into a fully-defaulted config", async () => { + const resolved = await parseConfig(fixture("valid-config.ts")); + expect(resolved.type).toBe("client"); + expect(resolved).toMatchObject({ + entryFile: "custom.ts", + out: { dir: "dist", mainFile: "main.js" }, + libs: [], + }); + }); + + it("resolves a .js config file the same way as .ts", async () => { + const resolved = await parseConfig(fixture("valid-config.js")); + expect(resolved).toEqual({ type: "workspace", packages: ["apps/*"] }); + }); + + it("rejects with code 'not-found' when the file doesn't exist", async () => { + await expect(parseConfig(fixture("missing-config.ts"))).rejects.toMatchObject({ + constructor: ConfigParseError, + code: "not-found", + }); + }); + + it("rejects with code 'load-failed' when the file throws while loading", async () => { + const error = await parseConfig(fixture("throwing-config.ts")).catch((e) => e); + expect(error).toMatchObject({ constructor: ConfigParseError, code: "load-failed" }); + expect(error.message).toContain("boom"); + }); + + it("rejects with code 'no-default-export' when there's no default export", async () => { + await expect(parseConfig(fixture("no-default-export.ts"))).rejects.toMatchObject({ + constructor: ConfigParseError, + code: "no-default-export", + }); + }); + + it("rejects with code 'invalid-type' when the type is unrecognized", async () => { + await expect(parseConfig(fixture("invalid-type-config.ts"))).rejects.toMatchObject({ + constructor: ConfigParseError, + code: "invalid-type", + }); + }); +}); diff --git a/libs/config/test/parse/fixtures/invalid-type-config.ts b/libs/config/test/parse/fixtures/invalid-type-config.ts new file mode 100644 index 0000000..850f8cd --- /dev/null +++ b/libs/config/test/parse/fixtures/invalid-type-config.ts @@ -0,0 +1,3 @@ +export default { + type: "nope", +}; diff --git a/libs/config/test/parse/fixtures/no-default-export.ts b/libs/config/test/parse/fixtures/no-default-export.ts new file mode 100644 index 0000000..f85a7a9 --- /dev/null +++ b/libs/config/test/parse/fixtures/no-default-export.ts @@ -0,0 +1,3 @@ +export const notDefault = { + type: "client", +}; diff --git a/libs/config/test/parse/fixtures/throwing-config.ts b/libs/config/test/parse/fixtures/throwing-config.ts new file mode 100644 index 0000000..c3fc99d --- /dev/null +++ b/libs/config/test/parse/fixtures/throwing-config.ts @@ -0,0 +1 @@ +throw new Error("boom"); diff --git a/libs/config/test/parse/fixtures/valid-config.js b/libs/config/test/parse/fixtures/valid-config.js new file mode 100644 index 0000000..e9727de --- /dev/null +++ b/libs/config/test/parse/fixtures/valid-config.js @@ -0,0 +1,4 @@ +export default { + type: "workspace", + packages: ["apps/*"], +}; diff --git a/libs/config/test/parse/fixtures/valid-config.ts b/libs/config/test/parse/fixtures/valid-config.ts new file mode 100644 index 0000000..ae4e5c1 --- /dev/null +++ b/libs/config/test/parse/fixtures/valid-config.ts @@ -0,0 +1,4 @@ +export default { + type: "client", + entryFile: "custom.ts", +}; diff --git a/libs/config/test/resolve-config.spec.ts b/libs/config/test/resolve-config.spec.ts new file mode 100644 index 0000000..5f60e27 --- /dev/null +++ b/libs/config/test/resolve-config.spec.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; + +import { + resolveClientConfig, + resolveConfig, + resolveLibConfig, + resolveServerConfig, + resolveWorkspaceConfig, +} from "../src"; + +describe("resolveWorkspaceConfig", () => { + it("fills in defaults for an empty config", () => { + expect(resolveWorkspaceConfig({ type: "workspace" })).toEqual({ + type: "workspace", + packages: [], + }); + }); + + it("lets an explicit override win", () => { + expect(resolveWorkspaceConfig({ type: "workspace", packages: ["apps/*"] })).toEqual({ + type: "workspace", + packages: ["apps/*"], + }); + }); +}); + +describe("resolveLibConfig", () => { + it("fills in nested dir defaults", () => { + expect(resolveLibConfig({ type: "lib" }).dir).toEqual({ + assets: "assets", + shared: "shared", + components: "shared/components", + systems: "shared/systems", + scenes: "shared/scenes", + }); + }); + + it("merges a partial dir override with defaults", () => { + expect(resolveLibConfig({ type: "lib", dir: { assets: "static" } }).dir).toEqual({ + assets: "static", + shared: "shared", + components: "shared/components", + systems: "shared/systems", + scenes: "shared/scenes", + }); + }); +}); + +describe("resolveClientConfig", () => { + it("fills in defaults for an empty config", () => { + const resolved = resolveClientConfig({ type: "client" }); + expect(resolved.entryFile).toBe("src/main.ts"); + expect(resolved.out).toEqual({ dir: "dist", mainFile: "main.js" }); + expect(resolved.libs).toEqual([]); + }); + + it("replaces array fields wholesale instead of concatenating", () => { + const resolved = resolveClientConfig({ type: "client", libs: ["../../libs/test-lib"] }); + expect(resolved.libs).toEqual(["../../libs/test-lib"]); + }); +}); + +describe("resolveServerConfig", () => { + it("fills in defaults for an empty config", () => { + const resolved = resolveServerConfig({ type: "server" }); + expect(resolved.entryFile).toBe("src/main.ts"); + expect(resolved.dir?.assets).toBe("assets"); + }); +}); + +describe("resolveConfig", () => { + it("dispatches to the resolver matching the config's type", () => { + expect(resolveConfig({ type: "workspace" })).toEqual({ type: "workspace", packages: [] }); + expect(resolveConfig({ type: "server" }).entryFile).toBe("src/main.ts"); + }); +}); diff --git a/libs/config/tsconfig.json b/libs/config/tsconfig.json new file mode 100644 index 0000000..1ada333 --- /dev/null +++ b/libs/config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "declaration": true, + "paths": { + "/snippets/*": ["./docs/snippets/*"] + } + }, + "mdx": { + "checkMdx": true + } +} diff --git a/libs/config/tsconfig.spec.json b/libs/config/tsconfig.spec.json new file mode 100644 index 0000000..8270cab --- /dev/null +++ b/libs/config/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "skipLibCheck": true + }, + "include": ["test/**/*.spec.ts"], + "exclude": ["node_modules"] +} diff --git a/libs/config/tsdown.config.ts b/libs/config/tsdown.config.ts new file mode 100644 index 0000000..609a944 --- /dev/null +++ b/libs/config/tsdown.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "tsdown"; + +export default [ + defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + format: ["esm", "cjs"], + shims: true, + dts: true, + sourcemap: true, + clean: true, + minify: true, + fixedExtension: false, + platform: "node", + target: "esnext", + treeshake: false, + deps: { + neverBundle: true, + }, + outputOptions: { + assetFileNames: "[name][extname]", + }, + }), +]; diff --git a/libs/config/vitest.config.ts b/libs/config/vitest.config.ts new file mode 100644 index 0000000..3ff3a4a --- /dev/null +++ b/libs/config/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.spec.ts"], + passWithNoTests: true, + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: [ + "**/*.{interface,type,d}.ts", + "**/{interfaces,types}/*.ts", + "**/{interfaces,types}.ts", + "**/index.{js,ts}", + ], + }, + }, +}); diff --git a/libs/schematics/.cliff-jumperrc.json b/libs/schematics/.cliff-jumperrc.json new file mode 100644 index 0000000..39ace0a --- /dev/null +++ b/libs/schematics/.cliff-jumperrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", + "name": "schematics", + "org": "nanoforge-dev", + "packagePath": "libs/schematics", + "tagTemplate": "{{new-version}}", + "identifierBase": false +} diff --git a/libs/schematics/.gitignore b/libs/schematics/.gitignore new file mode 100644 index 0000000..bca0f13 --- /dev/null +++ b/libs/schematics/.gitignore @@ -0,0 +1,226 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +.direnv diff --git a/libs/schematics/.prettierignore b/libs/schematics/.prettierignore new file mode 100644 index 0000000..91942fb --- /dev/null +++ b/libs/schematics/.prettierignore @@ -0,0 +1,14 @@ +.idea/ +.vscode/ + +dist/ +node_modules/ + +pnpm-lock.yaml +pnpm-workspace.yaml + +CHANGELOG.md + +coverage/ + +/src/libs/*/files/** diff --git a/libs/schematics/CHANGELOG.md b/libs/schematics/CHANGELOG.md new file mode 100644 index 0000000..911dfd8 --- /dev/null +++ b/libs/schematics/CHANGELOG.md @@ -0,0 +1,245 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +# [2.2.0](https://github.com/NanoForge-dev/schematics/compare/2.1.4...2.2.0) - (2026-06-30) + +## Features + +- Add pnpm workspace in app schematic (#155) ([5e0924d](https://github.com/NanoForge-dev/schematics/commit/5e0924d217c6ca62e322627f4adee79470c7bf2f)) by @Exeloo +- Add assets to entity generation (#153) ([a008fc6](https://github.com/NanoForge-dev/schematics/commit/a008fc6784cc4d15f3f09fc41efd9a0ea658c3a5)) by @Exeloo +- Graphics 2d editor generation (#154) ([89059f7](https://github.com/NanoForge-dev/schematics/commit/89059f7fbb5a505f3669bc691acbf377abd51fa6)) by @Exeloo + +# [2.1.4](https://github.com/NanoForge-dev/schematics/compare/1.0.2...2.1.4) - (2026-06-29) + +## Bug Fixes + +- Issues (#139) ([131e794](https://github.com/NanoForge-dev/schematics/commit/131e794601b9fdcb583ac52eab9ca44a1cf8531f)) by @Exeloo +- **synchronize-docs:** Use actions key to have the rights (#127) ([f73464d](https://github.com/NanoForge-dev/schematics/commit/f73464d09f08f14f2c82960fe9cef2dfde7c00dc)) by @MartinFillon +- Give package json path (#124) ([a31e265](https://github.com/NanoForge-dev/schematics/commit/a31e26522078c9b60a63d3d91f1eccda96dadac7)) by @MartinFillon +- **template:** New format (#113) ([1e89de7](https://github.com/NanoForge-dev/schematics/commit/1e89de7a536f75783c260cf1a0f0b05f99ffcf97)) by @Tchips46 +- Remove name formatting on app (#110) ([9478843](https://github.com/NanoForge-dev/schematics/commit/9478843b45a97714070babc6a7d41b8a9a6f2f65)) by @Exeloo +- Change save format in part base schematic (#105) ([6eaffe8](https://github.com/NanoForge-dev/schematics/commit/6eaffe8d94ce99727a8c27e1f7d5e3fa39a787ca)) by @Exeloo +- **docker:** Change base image as alpine doesnt have the right libs (#102) ([3b8c2b2](https://github.com/NanoForge-dev/schematics/commit/3b8c2b2476210b3df6a08415bb5f18b7cbdaf970)) by @MartinFillon +- Remove ts types in js example system in part base (#93) ([89555b4](https://github.com/NanoForge-dev/schematics/commit/89555b46ffa8f3a4e2f157ecd9fabb2202de1cd7)) by @Exeloo +- Fields does not replace on application (#86) ([d252953](https://github.com/NanoForge-dev/schematics/commit/d252953d4aef98a3ea505bb7bd45701f139d5599)) by @Exeloo +- Use of directory variable and js issues (#84) ([9d98d1a](https://github.com/NanoForge-dev/schematics/commit/9d98d1ae977e69cfc07b4e9131bde2ea2b34964f)) by @Exeloo + +## Documentation + +- Fix docs path (#140) ([fead8fd](https://github.com/NanoForge-dev/schematics/commit/fead8fd1e12ff872a5056f791dcaa7013bd135db)) by @Exeloo +- Add schematics docs (#61) ([7735324](https://github.com/NanoForge-dev/schematics/commit/7735324bcb5438929a24481e157677cc25f7457c)) by @Exeloo +- Fix contribution (#47) ([b6ad53e](https://github.com/NanoForge-dev/schematics/commit/b6ad53e9f77b6f6619b5090f08e9d3fb94e90a74)) by @Exeloo +- Add contributing docs and actions (#41) ([91eea94](https://github.com/NanoForge-dev/schematics/commit/91eea949ea785a03ca66d3a6e07d30d8e24df815)) by @Exeloo + +## Features + +- Add new release workflow (#137) ([af6c1e0](https://github.com/NanoForge-dev/schematics/commit/af6c1e0a9d49cb0cf46bbe6edde02ccfde04bc89)) by @Exeloo +- Use new docs actions to synchronize config (#123) ([fa3f4ec](https://github.com/NanoForge-dev/schematics/commit/fa3f4ece55b281bb37771b2f1fe5580ff5f707fc)) by @MartinFillon +- Add dom to application schema tsconfig (#111) ([368fa7c](https://github.com/NanoForge-dev/schematics/commit/368fa7cdd94027fa9b8cc23516654bd6bb3a0beb)) by @Exeloo +- Add component and system schematics (#103) ([38c90df](https://github.com/NanoForge-dev/schematics/commit/38c90df9bbe971da83aaa0338f39f875695083cf)) by @Exeloo +- **editor:** Add component holding entity id (#100) ([433c648](https://github.com/NanoForge-dev/schematics/commit/433c64851faafaf4198ac56801d09b47cce150c4)) by @Tchips46 +- **save:** Allow partial saves component values as record string any (#98) ([9280349](https://github.com/NanoForge-dev/schematics/commit/9280349d9028d99028ab529e3c477cfd11d14606)) by @Tchips46 +- Set save params types to any (#96) ([7a29579](https://github.com/NanoForge-dev/schematics/commit/7a29579e03fc1c4ca2ebc7794b4b3dd60167f741)) by @Exeloo +- Add core editor to dependencies (#90) ([29cfa08](https://github.com/NanoForge-dev/schematics/commit/29cfa0864ee4a84fdc5aed5e790da454334dd2d5)) by @Exeloo +- Add better handling of path option (#89) ([1e0c469](https://github.com/NanoForge-dev/schematics/commit/1e0c469f98aac91d81e21d20e36a7d9e7a94df8b)) by @Exeloo + - **BREAKING CHANGE:** Old schemas doesn't work anymore and config is changed +- Add editor option to part main schematic (#88) ([c59ed27](https://github.com/NanoForge-dev/schematics/commit/c59ed2710e2db2bcd3e5c553be9c80f5ad54b732)) by @Exeloo +- Add lint param handling on application (#79) ([96c2128](https://github.com/NanoForge-dev/schematics/commit/96c212817d763fc2677d28cd57f3d196c6a45f6d)) by @Exeloo +- Setup docker schema (#75) ([cf7ead5](https://github.com/NanoForge-dev/schematics/commit/cf7ead5c49164a85e3beb65be3c73c842cd14e58)) by @MartinFillon +- Add network and fix versions in app base (#70) ([6d5963a](https://github.com/NanoForge-dev/schematics/commit/6d5963a954e7a27c442377ecb70716a43e479717)) by @Exeloo +- Update and add pre-release (#48) ([49422f7](https://github.com/NanoForge-dev/schematics/commit/49422f7ad9e0595378cf6e8b44dc7711d09b8cfa)) by @Exeloo + +## Testing + +- Add unit tests and e2e tests (#67) ([8eef6b5](https://github.com/NanoForge-dev/schematics/commit/8eef6b51984d1187d3f0b936d670e362ad0224c6)) by @Exeloo + +### New Contributors + +- @MartinFillon made their first contribution in #151 +- @github-actions[bot] made their first contribution in #150 +- @dependabot[bot] made their first contribution in #143 +- @Tchips46 made their first contribution in #113 + +# [2.1.4](https://github.com/NanoForge-dev/schematics/compare/1.0.2...2.1.4) - (2026-06-29) + +## Bug Fixes + +- Issues (#139) ([131e794](https://github.com/NanoForge-dev/schematics/commit/131e794601b9fdcb583ac52eab9ca44a1cf8531f)) by @Exeloo +- **synchronize-docs:** Use actions key to have the rights (#127) ([f73464d](https://github.com/NanoForge-dev/schematics/commit/f73464d09f08f14f2c82960fe9cef2dfde7c00dc)) by @MartinFillon +- Give package json path (#124) ([a31e265](https://github.com/NanoForge-dev/schematics/commit/a31e26522078c9b60a63d3d91f1eccda96dadac7)) by @MartinFillon +- **template:** New format (#113) ([1e89de7](https://github.com/NanoForge-dev/schematics/commit/1e89de7a536f75783c260cf1a0f0b05f99ffcf97)) by @Tchips46 +- Remove name formatting on app (#110) ([9478843](https://github.com/NanoForge-dev/schematics/commit/9478843b45a97714070babc6a7d41b8a9a6f2f65)) by @Exeloo +- Change save format in part base schematic (#105) ([6eaffe8](https://github.com/NanoForge-dev/schematics/commit/6eaffe8d94ce99727a8c27e1f7d5e3fa39a787ca)) by @Exeloo +- **docker:** Change base image as alpine doesnt have the right libs (#102) ([3b8c2b2](https://github.com/NanoForge-dev/schematics/commit/3b8c2b2476210b3df6a08415bb5f18b7cbdaf970)) by @MartinFillon +- Remove ts types in js example system in part base (#93) ([89555b4](https://github.com/NanoForge-dev/schematics/commit/89555b46ffa8f3a4e2f157ecd9fabb2202de1cd7)) by @Exeloo +- Fields does not replace on application (#86) ([d252953](https://github.com/NanoForge-dev/schematics/commit/d252953d4aef98a3ea505bb7bd45701f139d5599)) by @Exeloo +- Use of directory variable and js issues (#84) ([9d98d1a](https://github.com/NanoForge-dev/schematics/commit/9d98d1ae977e69cfc07b4e9131bde2ea2b34964f)) by @Exeloo + +## Documentation + +- Fix docs path (#140) ([fead8fd](https://github.com/NanoForge-dev/schematics/commit/fead8fd1e12ff872a5056f791dcaa7013bd135db)) by @Exeloo +- Add schematics docs (#61) ([7735324](https://github.com/NanoForge-dev/schematics/commit/7735324bcb5438929a24481e157677cc25f7457c)) by @Exeloo +- Fix contribution (#47) ([b6ad53e](https://github.com/NanoForge-dev/schematics/commit/b6ad53e9f77b6f6619b5090f08e9d3fb94e90a74)) by @Exeloo +- Add contributing docs and actions (#41) ([91eea94](https://github.com/NanoForge-dev/schematics/commit/91eea949ea785a03ca66d3a6e07d30d8e24df815)) by @Exeloo + +## Features + +- Add new release workflow (#137) ([af6c1e0](https://github.com/NanoForge-dev/schematics/commit/af6c1e0a9d49cb0cf46bbe6edde02ccfde04bc89)) by @Exeloo +- Use new docs actions to synchronize config (#123) ([fa3f4ec](https://github.com/NanoForge-dev/schematics/commit/fa3f4ece55b281bb37771b2f1fe5580ff5f707fc)) by @MartinFillon +- Add dom to application schema tsconfig (#111) ([368fa7c](https://github.com/NanoForge-dev/schematics/commit/368fa7cdd94027fa9b8cc23516654bd6bb3a0beb)) by @Exeloo +- Add component and system schematics (#103) ([38c90df](https://github.com/NanoForge-dev/schematics/commit/38c90df9bbe971da83aaa0338f39f875695083cf)) by @Exeloo +- **editor:** Add component holding entity id (#100) ([433c648](https://github.com/NanoForge-dev/schematics/commit/433c64851faafaf4198ac56801d09b47cce150c4)) by @Tchips46 +- **save:** Allow partial saves component values as record string any (#98) ([9280349](https://github.com/NanoForge-dev/schematics/commit/9280349d9028d99028ab529e3c477cfd11d14606)) by @Tchips46 +- Set save params types to any (#96) ([7a29579](https://github.com/NanoForge-dev/schematics/commit/7a29579e03fc1c4ca2ebc7794b4b3dd60167f741)) by @Exeloo +- Add core editor to dependencies (#90) ([29cfa08](https://github.com/NanoForge-dev/schematics/commit/29cfa0864ee4a84fdc5aed5e790da454334dd2d5)) by @Exeloo +- Add better handling of path option (#89) ([1e0c469](https://github.com/NanoForge-dev/schematics/commit/1e0c469f98aac91d81e21d20e36a7d9e7a94df8b)) by @Exeloo + - **BREAKING CHANGE:** Old schemas doesn't work anymore and config is changed +- Add editor option to part main schematic (#88) ([c59ed27](https://github.com/NanoForge-dev/schematics/commit/c59ed2710e2db2bcd3e5c553be9c80f5ad54b732)) by @Exeloo +- Add lint param handling on application (#79) ([96c2128](https://github.com/NanoForge-dev/schematics/commit/96c212817d763fc2677d28cd57f3d196c6a45f6d)) by @Exeloo +- Setup docker schema (#75) ([cf7ead5](https://github.com/NanoForge-dev/schematics/commit/cf7ead5c49164a85e3beb65be3c73c842cd14e58)) by @MartinFillon +- Add network and fix versions in app base (#70) ([6d5963a](https://github.com/NanoForge-dev/schematics/commit/6d5963a954e7a27c442377ecb70716a43e479717)) by @Exeloo +- Update and add pre-release (#48) ([49422f7](https://github.com/NanoForge-dev/schematics/commit/49422f7ad9e0595378cf6e8b44dc7711d09b8cfa)) by @Exeloo + +## Testing + +- Add unit tests and e2e tests (#67) ([8eef6b5](https://github.com/NanoForge-dev/schematics/commit/8eef6b51984d1187d3f0b936d670e362ad0224c6)) by @Exeloo + +### New Contributors + +- @MartinFillon made their first contribution in #149 +- @dependabot[bot] made their first contribution in #143 +- @github-actions[bot] made their first contribution in #114 +- @Tchips46 made their first contribution in #113 + +# [@nanoforge-dev/schematics@2.1.3](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.1.2...@nanoforge-dev/schematics@2.1.3) - (2026-04-19) + +## Bug Fixes + +- **template:** New format (#113) ([1e89de7](https://github.com/NanoForge-dev/schematics/commit/1e89de7a536f75783c260cf1a0f0b05f99ffcf97)) by @Tchips46 + +# [@nanoforge-dev/schematics@2.1.2](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.1.1...@nanoforge-dev/schematics@2.1.2) - (2026-04-17) + +## Bug Fixes + +- Remove name formatting on app (#110) ([9478843](https://github.com/NanoForge-dev/schematics/commit/9478843b45a97714070babc6a7d41b8a9a6f2f65)) by @Exeloo + +## Features + +- Add dom to application schema tsconfig (#111) ([368fa7c](https://github.com/NanoForge-dev/schematics/commit/368fa7cdd94027fa9b8cc23516654bd6bb3a0beb)) by @Exeloo + +# [@nanoforge-dev/schematics@2.1.1](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.1.0...@nanoforge-dev/schematics@2.1.1) - (2026-04-02) + +## Bug Fixes + +- Change save format in part base schematic (#105) ([6eaffe8](https://github.com/NanoForge-dev/schematics/commit/6eaffe8d94ce99727a8c27e1f7d5e3fa39a787ca)) by @Exeloo + +# [@nanoforge-dev/schematics@2.1.0](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@2.0.0...@nanoforge-dev/schematics@2.1.0) - (2026-04-02) + +## Bug Fixes + +- **docker:** Change base image as alpine doesnt have the right libs (#102) ([3b8c2b2](https://github.com/NanoForge-dev/schematics/commit/3b8c2b2476210b3df6a08415bb5f18b7cbdaf970)) by @MartinFillon +- Remove ts types in js example system in part base (#93) ([89555b4](https://github.com/NanoForge-dev/schematics/commit/89555b46ffa8f3a4e2f157ecd9fabb2202de1cd7)) by @Exeloo + +## Features + +- Add component and system schematics (#103) ([38c90df](https://github.com/NanoForge-dev/schematics/commit/38c90df9bbe971da83aaa0338f39f875695083cf)) by @Exeloo +- **editor:** Add component holding entity id (#100) ([433c648](https://github.com/NanoForge-dev/schematics/commit/433c64851faafaf4198ac56801d09b47cce150c4)) by @Tchips46 +- **save:** Allow partial saves component values as record string any (#98) ([9280349](https://github.com/NanoForge-dev/schematics/commit/9280349d9028d99028ab529e3c477cfd11d14606)) by @Tchips46 +- Set save params types to any (#96) ([7a29579](https://github.com/NanoForge-dev/schematics/commit/7a29579e03fc1c4ca2ebc7794b4b3dd60167f741)) by @Exeloo + +### New Contributors + +- @Tchips46 made their first contribution in #100 + +# [@nanoforge-dev/schematics@2.0.0](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.2.2...@nanoforge-dev/schematics@2.0.0) - (2026-03-17) + +## Features + +- Add core editor to dependencies (#90) ([29cfa08](https://github.com/NanoForge-dev/schematics/commit/29cfa0864ee4a84fdc5aed5e790da454334dd2d5)) by @Exeloo +- Add better handling of path option (#89) ([1e0c469](https://github.com/NanoForge-dev/schematics/commit/1e0c469f98aac91d81e21d20e36a7d9e7a94df8b)) by @Exeloo + - **BREAKING CHANGE:** Old schemas doesn't work anymore and config is changed +- Add editor option to part main schematic (#88) ([c59ed27](https://github.com/NanoForge-dev/schematics/commit/c59ed2710e2db2bcd3e5c553be9c80f5ad54b732)) by @Exeloo + +# [@nanoforge-dev/schematics@1.2.2](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.2.1...@nanoforge-dev/schematics@1.2.2) - (2026-03-15) + +## Bug Fixes + +- Fields does not replace on application (#86) ([d252953](https://github.com/NanoForge-dev/schematics/commit/d252953d4aef98a3ea505bb7bd45701f139d5599)) by @Exeloo + +# [@nanoforge-dev/schematics@1.2.1](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.2.0...@nanoforge-dev/schematics@1.2.1) - (2026-03-15) + +## Bug Fixes + +- Use of directory variable and js issues (#84) ([9d98d1a](https://github.com/NanoForge-dev/schematics/commit/9d98d1ae977e69cfc07b4e9131bde2ea2b34964f)) by @Exeloo + +## Features + +- Add lint param handling on application (#79) ([96c2128](https://github.com/NanoForge-dev/schematics/commit/96c212817d763fc2677d28cd57f3d196c6a45f6d)) by @Exeloo + +# [@nanoforge-dev/schematics@1.2.0](https://github.com/NanoForge-dev/schematics/compare/@nanoforge-dev/schematics@1.1.0...@nanoforge-dev/schematics@1.2.0) - (2026-02-27) + +## Features + +- Setup docker schema (#75) ([cf7ead5](https://github.com/NanoForge-dev/schematics/commit/cf7ead5c49164a85e3beb65be3c73c842cd14e58)) by @MartinFillon + +### New Contributors + +- @MartinFillon made their first contribution in #75 +- @dependabot[bot] made their first contribution in #76 + +# [@nanoforge-dev/schematics@1.1.0](https://github.com/NanoForge-dev/schematics/tree/@nanoforge-dev/schematics@1.1.0) - (2026-02-19) + +## Documentation + +- Add schematics docs (#61) ([7735324](https://github.com/NanoForge-dev/schematics/commit/7735324bcb5438929a24481e157677cc25f7457c)) by @Exeloo +- Fix contribution (#47) ([b6ad53e](https://github.com/NanoForge-dev/schematics/commit/b6ad53e9f77b6f6619b5090f08e9d3fb94e90a74)) by @Exeloo +- Add contributing docs and actions (#41) ([91eea94](https://github.com/NanoForge-dev/schematics/commit/91eea949ea785a03ca66d3a6e07d30d8e24df815)) by @Exeloo + +## Features + +- Add network and fix versions in app base (#70) ([6d5963a](https://github.com/NanoForge-dev/schematics/commit/6d5963a954e7a27c442377ecb70716a43e479717)) by @Exeloo +- Update and add pre-release (#48) ([49422f7](https://github.com/NanoForge-dev/schematics/commit/49422f7ad9e0595378cf6e8b44dc7711d09b8cfa)) by @Exeloo + +## Testing + +- Add unit tests and e2e tests (#67) ([8eef6b5](https://github.com/NanoForge-dev/schematics/commit/8eef6b51984d1187d3f0b936d670e362ad0224c6)) by @Exeloo + +# [1.0.2](https://github.com/NanoForge-dev/schematics/compare/1.0.1...1.0.2) - (2025-12-06) + +## Bug Fixes + +- Split ecs between client and server (#38) ([086d42f](https://github.com/NanoForge-dev/schematics/commit/086d42f1639cc764fb6f21c6ce8d9426ec2581e7)) by @Exeloo + +# [1.0.1](https://github.com/NanoForge-dev/schematics/compare/1.0.0...1.0.1) - (2025-12-04) + +## Bug Fixes + +- **configuration:** Simplify schema and change defaults (#34) ([af29b00](https://github.com/NanoForge-dev/schematics/commit/af29b0011ae078dc75c9625e8f470f8506a7c7cb)) by @Exeloo + +# [1.0.0](https://github.com/NanoForge-dev/schematics/compare/0.0.1...1.0.0) - (2025-11-30) + +## Bug Fixes + +- Change exports in package json and update schema paths (#17) ([b78b72a](https://github.com/NanoForge-dev/schematics/commit/b78b72a40b2287d841a3cac90adb0d37b4493d4e)) by @Exeloo + +## Features + +- **schematics:** Add part-main schematics for generating client/server main files (#29) ([6f34c1c](https://github.com/NanoForge-dev/schematics/commit/6f34c1c0f70d67eb78645108a665f7c5ebab8086)) by @Exeloo +- **schematics:** Replace client by base-part schematics to handle server (#20) ([c5ad539](https://github.com/NanoForge-dev/schematics/commit/c5ad539d7ac524de724c49dfa6b0e7b310700a44)) by @Exeloo +- **schematics:** Add client base schematics (#19) ([f02b26d](https://github.com/NanoForge-dev/schematics/commit/f02b26db6be9226857fbf3fe0ffe1abf6de31024)) by @Exeloo + +### New Contributors + +- @renovate[bot] made their first contribution in #27 + +# [0.0.1](https://github.com/NanoForge-dev/schematics/tree/0.0.1) - (2025-11-28) + +## Features + +- Add base application and configuration schematics (#12) ([d881653](https://github.com/NanoForge-dev/schematics/commit/d881653dd57a886f12e18b9acf0d48d31d3686e6)) +- Init schematics project (#11) ([3fb0e61](https://github.com/NanoForge-dev/schematics/commit/3fb0e61075cf8094309765a0f685d2411c7f8aa8)) by @Exeloo + +### New Contributors + +- @Exeloo made their first contribution in #11 diff --git a/libs/schematics/LICENSE b/libs/schematics/LICENSE new file mode 100644 index 0000000..4672486 --- /dev/null +++ b/libs/schematics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2026 NanoForge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/schematics/README.md b/libs/schematics/README.md new file mode 100644 index 0000000..90ab999 --- /dev/null +++ b/libs/schematics/README.md @@ -0,0 +1,58 @@ +
+
+

+ NanoForge +

+
+

+ npm version + npm downloads + Tests status + Release status + Last commit + Contributors +

+
+ +## About + +This package contains the Schematics of NanoForge, used by the [CLI][cli-source] to generate project files. Check [releases][github-releases] to see versions of the Schematics. Nanoforge is a powerful game engine for web browser. + +## Usage + +To use Nanoforge Schematics, please refer to the [CLI documentation][cli-source] ! + +First, install the CLI : + +```bash +npm install -g @nanoforge-dev/cli +``` + +And then create a new project : + +```bash +nf new +``` + +## Schematics + +This repository provide multiples schematics, usable with Angular Devkit schematics. + +- `workspace` : Root of a NanoForge monorepo (`nanoforge.config.ts`, `package.json`, tooling) +- `project` : Client or server project under `apps/`, including `main.ts`, example component/system, and an optional Dockerfile +- `component` : ECS component class for a client or server part +- `system` : ECS system function for a client or server part + +## Contributing + +Please read through our [contribution guidelines][contributing] before starting a pull request. We welcome contributions of all kinds, not just code! If you're stuck for ideas, look for the [good first issue][good-first-issue] label on issues in the repository. If you have any questions about the project, feel free to ask them on [Discussions][discussions]. Before creating your own issue or pull request, always check to see if one already exists! Don't rush contributions, take your time and ensure you're doing it correctly. + +## Help + +If you don't understand something in the documentation, you are experiencing problems, or you just need a gentle nudge in the right direction, please ask on [Discussions][discussions]. + +[contributing]: https://github.com/NanoForge-dev/CLI/blob/main/.github/CONTRIBUTING.md +[discussions]: https://github.com/NanoForge-dev/CLI/discussions +[cli-source]: https://github.com/NanoForge-dev/CLI +[github-releases]: https://github.com/NanoForge-dev/CLI/releases +[good-first-issue]: https://github.com/NanoForge-dev/CLI/contribute diff --git a/libs/schematics/cliff.toml b/libs/schematics/cliff.toml new file mode 100644 index 0000000..4bb6a65 --- /dev/null +++ b/libs/schematics/cliff.toml @@ -0,0 +1,79 @@ +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file.\n +""" +body = """ +{%- macro remote_url() -%} + https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} +{%- endmacro -%} +{% if version %}\ + # [{{ version | trim_start_matches(pat="v") }}]\ + {% if previous %}\ + {% if previous.version %}\ + ({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\ + {% else %}\ + ({{ self::remote_url() }}/tree/{{ version }})\ + {% endif %}\ + {% endif %} \ + - ({{ timestamp | date(format="%Y-%m-%d") }}) +{% else %}\ + # [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ## {{ group | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}\ + **{{commit.scope}}:** \ + {% endif %}\ + {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ + {% if commit.breaking %}\ + {% for footer in commit.footers %}\ + {% if footer.breaking %}\ + \n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\ + {% endif %}\ + {% endfor %}\ + {% endif %}\ + {% endfor %} +{% endfor %}\ +{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\ + \n### New Contributors\n + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\ + - @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }} + {% endfor %}\ +{% endif %}\n +""" +trim = true +footer = "" + +[git] +conventional_commits = true +filter_unconventional = true +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^docs", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^refactor", group = "Refactor"}, + { message = "^types", group = "Typings"}, + { message = ".*deprecated", body = ".*deprecated", group = "Deprecation"}, + { message = "^revert", skip = true}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^chore", skip = true}, + { message = "^ci", skip = true}, + { message = "^build", skip = true}, + { body = ".*security", group = "Security"}, +] +filter_commits = true +protect_breaking_commits = true +tag_pattern = "^[0-9]+" +ignore_tags = "" +topo_order = false +sort_commits = "newest" + +[remote.github] +owner = "NanoForge-dev" +repo = "CLI" diff --git a/libs/schematics/commitlint.config.cjs b/libs/schematics/commitlint.config.cjs new file mode 100644 index 0000000..a6580b4 --- /dev/null +++ b/libs/schematics/commitlint.config.cjs @@ -0,0 +1,4 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: {}, +}; diff --git a/libs/schematics/e2e/component.test.ts b/libs/schematics/e2e/component.test.ts new file mode 100644 index 0000000..aaf3cf8 --- /dev/null +++ b/libs/schematics/e2e/component.test.ts @@ -0,0 +1,171 @@ +import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing"; +import { resolve } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; + +const collectionPath = resolve(__dirname, "../dist/collection.json"); + +describe("component schematic", () => { + const runner = new SchematicTestRunner("schematics", collectionPath); + + describe("TypeScript component for client part", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("component", { + name: "myComponent", + directory: "my-app", + part: "client", + language: "ts", + }); + }); + + it("should generate a .ts component file", () => { + expect(tree.files).toContain("/my-app/my-component.component.ts"); + }); + + it("should use PascalCase class name", () => { + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("class MyComponentComponent"); + }); + + it("should use the correct part in the import", () => { + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("@nanoforge-dev/ecs-client"); + }); + + it("should export the component name as default", () => { + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("export default MyComponentComponent.name"); + }); + + it("should export the EDITOR_COMPONENT_MANIFEST", () => { + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("export const EDITOR_COMPONENT_MANIFEST"); + expect(content).toContain('name: "MyComponent"'); + }); + }); + + describe("TypeScript component for server part", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("component", { + name: "myComponent", + directory: "my-app", + part: "server", + language: "ts", + }); + }); + + it("should generate a .ts component file", () => { + expect(tree.files).toContain("/my-app/my-component.component.ts"); + }); + + it("should use the correct part in the import", () => { + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("@nanoforge-dev/ecs-server"); + }); + }); + + describe("JavaScript component", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("component", { + name: "myComponent", + directory: "my-app", + part: "client", + language: "js", + }); + }); + + it("should generate a .js component file", () => { + expect(tree.files).toContain("/my-app/my-component.component.js"); + }); + + it("should use PascalCase class name", () => { + const content = tree.readContent("/my-app/my-component.component.js"); + expect(content).toContain("class MyComponentComponent"); + }); + + it("should use the correct part in the JSDoc typedef", () => { + const content = tree.readContent("/my-app/my-component.component.js"); + expect(content).toContain("@nanoforge-dev/ecs-client"); + }); + + it("should export the EDITOR_COMPONENT_MANIFEST", () => { + const content = tree.readContent("/my-app/my-component.component.js"); + expect(content).toContain("export const EDITOR_COMPONENT_MANIFEST"); + expect(content).toContain('name: "MyComponent"'); + }); + }); + + describe("name formatting", () => { + it("should handle kebab-case name", async () => { + const tree = await runner.runSchematic("component", { + name: "my-component", + directory: "my-app", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/my-app/my-component.component.ts"); + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("class MyComponentComponent"); + }); + + it("should handle PascalCase name", async () => { + const tree = await runner.runSchematic("component", { + name: "MyComponent", + directory: "my-app", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/my-app/my-component.component.ts"); + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("class MyComponentComponent"); + }); + + it("should handle snake_case name", async () => { + const tree = await runner.runSchematic("component", { + name: "my_component", + directory: "my-app", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/my-app/my-component.component.ts"); + const content = tree.readContent("/my-app/my-component.component.ts"); + expect(content).toContain("class MyComponentComponent"); + }); + }); + + describe("custom directory", () => { + it("should generate file in the specified directory", async () => { + const tree = await runner.runSchematic("component", { + name: "myComponent", + directory: "src/client/components", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/src/client/components/my-component.component.ts"); + }); + }); + + describe("default values", () => { + it("should default to TypeScript when language is not specified", async () => { + const tree = await runner.runSchematic("component", { + name: "myComponent", + directory: "my-app", + part: "client", + }); + expect(tree.files).toContain("/my-app/my-component.component.ts"); + }); + + it("should default to current directory when directory is not specified", async () => { + const tree = await runner.runSchematic("component", { + name: "myComponent", + part: "client", + }); + expect(tree.files).toContain("/my-component.component.ts"); + }); + }); +}); diff --git a/libs/schematics/e2e/project.test.ts b/libs/schematics/e2e/project.test.ts new file mode 100644 index 0000000..2f0637e --- /dev/null +++ b/libs/schematics/e2e/project.test.ts @@ -0,0 +1,429 @@ +import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing"; +import { resolve } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; + +const collectionPath = resolve(__dirname, "../dist/collection.json"); + +// The factory resolves real registry versions (respecting a 48h trust +// window) for standalone projects, so we can't assert an exact pinned +// number here — only that a registry-resolved value or a workspace-linking +// protocol came back. +// Includes semver-range fallbacks (e.g. "^2") so this still passes when the +// registry is unreachable and the fetcher falls back to its default. +const RESOLVED_VERSION = /^(\d+\.\d+\.\d+|workspace:\*|\*|\^\d+(\.\d+){0,2})$/; + +describe("project schematic", () => { + const runner = new SchematicTestRunner("schematics", collectionPath); + + describe("client project with TypeScript", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "my-workspace", + directory: "apps/client", + }); + }); + + it("should generate the project structure under apps/client", () => { + expect(tree.files).toContain("/apps/client/package.json"); + expect(tree.files).toContain("/apps/client/tsconfig.json"); + expect(tree.files).toContain("/apps/client/nanoforge.config.ts"); + expect(tree.files).toContain("/apps/client/src/main.ts"); + expect(tree.files).toContain("/apps/client/src/components/drawable-circle-2d.component.ts"); + expect(tree.files).toContain("/apps/client/src/components/position-2d.component.ts"); + expect(tree.files).toContain("/apps/client/src/systems/draw-2d.system.ts"); + expect(tree.files).toContain("/apps/client/assets/.gitkeep"); + }); + + it("should derive the package name from workspaceName and part", () => { + const packageJson = JSON.parse(tree.readContent("/apps/client/package.json")); + expect(packageJson.name).toBe("my-workspace-client"); + }); + + it("should include client-only dependencies", () => { + const packageJson = JSON.parse(tree.readContent("/apps/client/package.json")); + expect(packageJson.devDependencies["@nanoforge-dev/graphics-2d"]).toBeDefined(); + expect(packageJson.devDependencies["@nanoforge-dev/input"]).toBeDefined(); + }); + + it("should declare network as a dev dependency", () => { + const packageJson = JSON.parse(tree.readContent("/apps/client/package.json")); + expect(packageJson.devDependencies["@nanoforge-dev/network"]).toBeDefined(); + expect(packageJson).not.toHaveProperty("dependencies"); + }); + + it("should declare a client-typed nanoforge.config.ts", () => { + const content = tree.readContent("/apps/client/nanoforge.config.ts"); + expect(content).toContain('type: "client"'); + }); + + it("should generate a client entry point", () => { + const content = tree.readContent("/apps/client/src/main.ts"); + expect(content).toContain("NanoforgeFactory.createClient({ tickRate: 60 })"); + expect(content).toContain('from "@nanoforge-dev/ecs/client"'); + expect(content).toContain("Graphics2DLibrary"); + expect(content).toContain("InputLibrary"); + expect(content).toContain("NetworkClientLibrary"); + }); + + it("should access the graphics library directly off ctx, not through ctx.libs", () => { + const content = tree.readContent("/apps/client/src/systems/draw-2d.system.ts"); + expect(content).toContain("ctx.graphics"); + expect(content).not.toContain("ctx.libs"); + }); + + it("should not generate Dockerfile or init functions by default", () => { + expect(tree.files).not.toContain("/apps/client/Dockerfile"); + expect(tree.files).not.toContain("/apps/client/.dockerignore"); + const initFiles = tree.files.filter((f) => f.includes("/init/")); + expect(initFiles).toHaveLength(0); + }); + }); + + describe("server project with TypeScript", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("project", { + part: "server", + workspaceName: "my-workspace", + directory: "apps/server", + }); + }); + + it("should generate the project structure under apps/server", () => { + expect(tree.files).toContain("/apps/server/package.json"); + expect(tree.files).toContain("/apps/server/src/main.ts"); + }); + + it("should not include client-only dependencies", () => { + const packageJson = JSON.parse(tree.readContent("/apps/server/package.json")); + expect(packageJson.devDependencies["@nanoforge-dev/graphics-2d"]).toBeUndefined(); + expect(packageJson.devDependencies["@nanoforge-dev/input"]).toBeUndefined(); + }); + + it("should declare network as a runtime dependency", () => { + const packageJson = JSON.parse(tree.readContent("/apps/server/package.json")); + expect(packageJson.dependencies["@nanoforge-dev/network"]).toBeDefined(); + expect(packageJson.devDependencies["@nanoforge-dev/network"]).toBeUndefined(); + }); + + it("should generate a server entry point", () => { + const content = tree.readContent("/apps/server/src/main.ts"); + expect(content).toContain("NanoforgeFactory.createServer({ tickRate: 60 })"); + expect(content).toContain('from "@nanoforge-dev/ecs/server"'); + expect(content).not.toContain("Graphics2DLibrary"); + expect(content).toContain("NetworkServerLibrary"); + expect(content).toContain("move2D"); + expect(content).toContain("new Position2D(500, 500)"); + }); + + it("should generate a server-side position component and movement system", () => { + expect(tree.files).toContain("/apps/server/src/components/position-2d.component.ts"); + expect(tree.files).toContain("/apps/server/src/systems/move-2d.system.ts"); + expect(tree.files).not.toContain( + "/apps/server/src/components/drawable-circle-2d.component.ts", + ); + expect(tree.files).not.toContain("/apps/server/src/systems/draw-2d.system.ts"); + }); + }); + + describe("with a custom name", () => { + it("replaces part in the package name when workspaceName is set", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "my-workspace", + name: "frontend", + directory: "custom-name-apps/client", + }); + const packageJson = JSON.parse(tree.readContent("/custom-name-apps/client/package.json")); + expect(packageJson.name).toBe("my-workspace-frontend"); + }); + + it("becomes the whole package name when workspaceName is absent", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + name: "standalone-app", + directory: "custom-name-standalone-apps/client", + }); + const packageJson = JSON.parse( + tree.readContent("/custom-name-standalone-apps/client/package.json"), + ); + expect(packageJson.name).toBe("standalone-app"); + }); + + it("falls back to a default app name when neither workspaceName nor name is set", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + directory: "custom-name-default-apps/client", + }); + const packageJson = JSON.parse( + tree.readContent("/custom-name-default-apps/client/package.json"), + ); + expect(packageJson.name).toBe("nanoforge-app"); + }); + }); + + describe("with hasServer", () => { + it("keeps the client's position local by default (hasServer omitted)", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "no-server-workspace", + directory: "no-server-apps/client", + }); + expect(tree.files).not.toContain( + "/no-server-apps/client/src/systems/position-sync.system.ts", + ); + const content = tree.readContent("/no-server-apps/client/src/main.ts"); + expect(content).not.toContain("positionSync"); + }); + + it("syncs the client's position from the server when hasServer is true", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "has-server-workspace", + directory: "has-server-apps/client", + hasServer: true, + }); + expect(tree.files).toContain("/has-server-apps/client/src/systems/position-sync.system.ts"); + const content = tree.readContent("/has-server-apps/client/src/main.ts"); + expect(content).toContain('import { positionSync } from "./systems/position-sync.system"'); + expect(content).toContain("registry.addSystem(positionSync)"); + }); + + it("is ignored for a server project - position handling is always active there", async () => { + const tree = await runner.runSchematic("project", { + part: "server", + workspaceName: "server-has-server-workspace", + directory: "server-has-server-apps/server", + hasServer: false, + }); + expect(tree.files).toContain("/server-has-server-apps/server/src/systems/move-2d.system.ts"); + }); + }); + + describe("with docker enabled", () => { + it("should generate Dockerfile and .dockerignore for a standalone project", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "docker-workspace", + directory: "docker-apps/client", + docker: true, + workspace: false, + }); + expect(tree.files).toContain("/docker-apps/client/Dockerfile"); + expect(tree.files).toContain("/docker-apps/client/.dockerignore"); + }); + + it("should not generate a Dockerfile when the project is inside a workspace", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "docker-in-workspace", + directory: "docker-in-workspace-apps/client", + docker: true, + workspace: true, + }); + expect(tree.files).not.toContain("/docker-in-workspace-apps/client/Dockerfile"); + expect(tree.files).not.toContain("/docker-in-workspace-apps/client/.dockerignore"); + }); + + describe.each(["npm", "yarn", "pnpm", "bun"] as const)( + "with %s package manager", + (packageManager) => { + it("should branch Dockerfile content on the package manager", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: `${packageManager}-workspace`, + directory: `${packageManager}-apps/client`, + docker: true, + workspace: false, + packageManager, + }); + const content = tree.readContent(`/${packageManager}-apps/client/Dockerfile`); + const installCommand = packageManager === "npm" ? "npm ci" : `${packageManager} install`; + expect(content).toContain(`RUN ${installCommand}`); + expect(content).toContain(`RUN ${packageManager} run build`); + expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`); + }); + }, + ); + }); + + describe("dependency versioning via the workspace field", () => { + describe.each(["npm", "yarn", "pnpm", "bun"] as const)( + "with %s package manager", + (packageManager) => { + it("resolves a real registry version by default", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: `${packageManager}-dep-workspace`, + directory: `${packageManager}-dep-apps/client`, + packageManager, + }); + const packageJson = JSON.parse( + tree.readContent(`/${packageManager}-dep-apps/client/package.json`), + ); + expect(packageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION); + expect(packageJson.devDependencies["nanoforge"]).toMatch(RESOLVED_VERSION); + }); + + it("resolves a real registry version when workspace is false", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: `${packageManager}-standalone-workspace`, + directory: `${packageManager}-standalone-apps/client`, + packageManager, + workspace: false, + }); + const packageJson = JSON.parse( + tree.readContent(`/${packageManager}-standalone-apps/client/package.json`), + ); + expect(packageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION); + expect(packageJson.devDependencies["nanoforge"]).toMatch(RESOLVED_VERSION); + }); + }, + ); + + it("produces valid JSON for a server project", async () => { + const tree = await runner.runSchematic("project", { + part: "server", + workspaceName: "valid-json-workspace", + directory: "valid-json-apps/server", + }); + expect(() => + JSON.parse(tree.readContent("/valid-json-apps/server/package.json")), + ).not.toThrow(); + }); + }); + + describe("with JavaScript", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "js-workspace", + directory: "js-apps/client", + language: "js", + }); + }); + + it("should generate JS files", () => { + expect(tree.files).toContain("/js-apps/client/src/main.js"); + expect(tree.files).toContain( + "/js-apps/client/src/components/drawable-circle-2d.component.js", + ); + expect(tree.files).toContain("/js-apps/client/jsconfig.json"); + expect(tree.files).not.toContain("/js-apps/client/tsconfig.json"); + }); + }); + + describe("@nanoforge-dev/ecs and @nanoforge-dev/network version resolution", () => { + it("still writes the ecs/network keys even though they're resolved via their real per-part packages", async () => { + // @nanoforge-dev/ecs and @nanoforge-dev/network aren't published under + // those names (see ecs-lib/ecs-client/ecs-server and + // network-client/network-server) — the factory aliases to the real + // package to resolve a trustworthy version, but keeps writing the + // existing (unchanged) `ecs`/`network` keys. + const client = await runner.runSchematic("project", { + part: "client", + workspaceName: "ecs-alias-client-workspace", + directory: "ecs-alias-client-apps/client", + workspace: false, + }); + const clientPackageJson = JSON.parse( + client.readContent("/ecs-alias-client-apps/client/package.json"), + ); + expect(clientPackageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION); + expect(clientPackageJson.devDependencies["@nanoforge-dev/network"]).toMatch(RESOLVED_VERSION); + + const server = await runner.runSchematic("project", { + part: "server", + workspaceName: "ecs-alias-server-workspace", + directory: "ecs-alias-server-apps/server", + workspace: false, + }); + const serverPackageJson = JSON.parse( + server.readContent("/ecs-alias-server-apps/server/package.json"), + ); + expect(serverPackageJson.devDependencies["@nanoforge-dev/ecs"]).toMatch(RESOLVED_VERSION); + expect(serverPackageJson.dependencies["@nanoforge-dev/network"]).toMatch(RESOLVED_VERSION); + }); + }); + + describe("packageManager (Corepack) field", () => { + it("is never set on a generated project's package.json", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "no-corepack-workspace", + directory: "no-corepack-apps/client", + workspace: false, + packageManager: "pnpm", + }); + const packageJson = JSON.parse(tree.readContent("/no-corepack-apps/client/package.json")); + expect(packageJson.packageManager).toBeUndefined(); + }); + }); + + describe("allowBuilds (standalone projects are their own root)", () => { + it("is empty when generated inside a workspace", async () => { + const tree = await runner.runSchematic("project", { + part: "server", + workspaceName: "in-workspace-allow-builds", + directory: "in-workspace-allow-builds-apps/server", + packageManager: "pnpm", + }); + expect(tree.files).not.toContain( + "/in-workspace-allow-builds-apps/server/pnpm-workspace.yaml", + ); + const packageJson = JSON.parse( + tree.readContent("/in-workspace-allow-builds-apps/server/package.json"), + ); + expect(packageJson).not.toHaveProperty("allowScripts"); + expect(packageJson).not.toHaveProperty("trustedDependencies"); + }); + + it("always allows bun in a pnpm-workspace.yaml for a standalone pnpm project", async () => { + const tree = await runner.runSchematic("project", { + part: "client", + workspaceName: "standalone-allow-builds", + directory: "standalone-allow-builds-apps/client", + workspace: false, + packageManager: "pnpm", + }); + const pnpmWorkspaceYaml = tree.readContent( + "/standalone-allow-builds-apps/client/pnpm-workspace.yaml", + ); + expect(pnpmWorkspaceYaml).toContain("allowBuilds:"); + expect(pnpmWorkspaceYaml).toContain("bun: true"); + }); + + it("always allows bun in allowScripts/trustedDependencies for a standalone npm/bun project", async () => { + const npmTree = await runner.runSchematic("project", { + part: "server", + workspaceName: "standalone-npm-allow-builds", + directory: "standalone-npm-allow-builds-apps/server", + workspace: false, + packageManager: "npm", + }); + const npmPackageJson = JSON.parse( + npmTree.readContent("/standalone-npm-allow-builds-apps/server/package.json"), + ); + expect(npmPackageJson.allowScripts).toEqual({ bun: true }); + + const bunTree = await runner.runSchematic("project", { + part: "server", + workspaceName: "standalone-bun-allow-builds", + directory: "standalone-bun-allow-builds-apps/server", + workspace: false, + packageManager: "bun", + }); + const bunPackageJson = JSON.parse( + bunTree.readContent("/standalone-bun-allow-builds-apps/server/package.json"), + ); + expect(bunPackageJson.trustedDependencies).toEqual(["bun"]); + }); + }); +}); diff --git a/libs/schematics/e2e/system.test.ts b/libs/schematics/e2e/system.test.ts new file mode 100644 index 0000000..b392fc3 --- /dev/null +++ b/libs/schematics/e2e/system.test.ts @@ -0,0 +1,171 @@ +import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing"; +import { resolve } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; + +const collectionPath = resolve(__dirname, "../dist/collection.json"); + +describe("system schematic", () => { + const runner = new SchematicTestRunner("schematics", collectionPath); + + describe("TypeScript system for client part", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("system", { + name: "mySystem", + directory: "my-app", + part: "client", + language: "ts", + }); + }); + + it("should generate a .ts system file", () => { + expect(tree.files).toContain("/my-app/my-system.system.ts"); + }); + + it("should use camelCase function name", () => { + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("export const mySystemSystem"); + }); + + it("should use the correct part in the import", () => { + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("@nanoforge-dev/ecs-client"); + }); + + it("should export the system name as default", () => { + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("export default mySystemSystem.name"); + }); + + it("should export the EDITOR_SYSTEM_MANIFEST", () => { + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("export const EDITOR_SYSTEM_MANIFEST"); + expect(content).toContain('name: "mySystem"'); + }); + }); + + describe("TypeScript system for server part", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("system", { + name: "mySystem", + directory: "my-app", + part: "server", + language: "ts", + }); + }); + + it("should generate a .ts system file", () => { + expect(tree.files).toContain("/my-app/my-system.system.ts"); + }); + + it("should use the correct part in the import", () => { + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("@nanoforge-dev/ecs-server"); + }); + }); + + describe("JavaScript system", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("system", { + name: "mySystem", + directory: "my-app", + part: "client", + language: "js", + }); + }); + + it("should generate a .js system file", () => { + expect(tree.files).toContain("/my-app/my-system.system.js"); + }); + + it("should use camelCase function name", () => { + const content = tree.readContent("/my-app/my-system.system.js"); + expect(content).toContain("export const mySystemSystem"); + }); + + it("should use the correct part in the JSDoc typedef", () => { + const content = tree.readContent("/my-app/my-system.system.js"); + expect(content).toContain("@nanoforge-dev/ecs-client"); + }); + + it("should export the EDITOR_SYSTEM_MANIFEST", () => { + const content = tree.readContent("/my-app/my-system.system.js"); + expect(content).toContain("export const EDITOR_SYSTEM_MANIFEST"); + expect(content).toContain('name: "mySystem"'); + }); + }); + + describe("name formatting", () => { + it("should handle kebab-case name", async () => { + const tree = await runner.runSchematic("system", { + name: "my-system", + directory: "my-app", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/my-app/my-system.system.ts"); + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("export const mySystemSystem"); + }); + + it("should handle PascalCase name", async () => { + const tree = await runner.runSchematic("system", { + name: "MySystem", + directory: "my-app", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/my-app/my-system.system.ts"); + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("export const mySystemSystem"); + }); + + it("should handle snake_case name", async () => { + const tree = await runner.runSchematic("system", { + name: "my_system", + directory: "my-app", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/my-app/my-system.system.ts"); + const content = tree.readContent("/my-app/my-system.system.ts"); + expect(content).toContain("export const mySystemSystem"); + }); + }); + + describe("custom directory", () => { + it("should generate file in the specified directory", async () => { + const tree = await runner.runSchematic("system", { + name: "mySystem", + directory: "src/client/systems", + part: "client", + language: "ts", + }); + expect(tree.files).toContain("/src/client/systems/my-system.system.ts"); + }); + }); + + describe("default values", () => { + it("should default to TypeScript when language is not specified", async () => { + const tree = await runner.runSchematic("system", { + name: "mySystem", + directory: "my-app", + part: "client", + }); + expect(tree.files).toContain("/my-app/my-system.system.ts"); + }); + + it("should default to current directory when directory is not specified", async () => { + const tree = await runner.runSchematic("system", { + name: "mySystem", + part: "client", + }); + expect(tree.files).toContain("/my-system.system.ts"); + }); + }); +}); diff --git a/libs/schematics/e2e/tsconfig.json b/libs/schematics/e2e/tsconfig.json new file mode 100644 index 0000000..a0601d8 --- /dev/null +++ b/libs/schematics/e2e/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["./**/*.test.ts"], + "exclude": [] +} diff --git a/libs/schematics/e2e/workspace.test.ts b/libs/schematics/e2e/workspace.test.ts new file mode 100644 index 0000000..10445b1 --- /dev/null +++ b/libs/schematics/e2e/workspace.test.ts @@ -0,0 +1,264 @@ +import { SchematicTestRunner, type UnitTestTree } from "@angular-devkit/schematics/testing"; +import { resolve } from "node:path"; +import { beforeAll, describe, expect, it } from "vitest"; + +const collectionPath = resolve(__dirname, "../dist/collection.json"); + +// The factory resolves real registry versions (respecting a 48h trust +// window), so we can't assert exact pinned numbers here — only that a +// registry-resolved value or a workspace-linking protocol came back. +// Includes semver-range fallbacks (e.g. "latest") so this still passes when +// the registry is unreachable and the fetcher falls back to its default. +const RESOLVED_VERSION = /^(\d+\.\d+\.\d+|workspace:\*|\*|latest|\^\d+(\.\d+){0,2})$/; + +describe("workspace schematic", () => { + const runner = new SchematicTestRunner("schematics", collectionPath); + + describe("with TypeScript (default)", () => { + let tree: UnitTestTree; + let packageJson: any; + + beforeAll(async () => { + tree = await runner.runSchematic("workspace", { name: "my-workspace" }); + packageJson = JSON.parse(tree.readContent("/my-workspace/package.json")); + }); + + it("should generate workspace files", () => { + expect(tree.files).toContain("/my-workspace/package.json"); + expect(tree.files).toContain("/my-workspace/tsconfig.json"); + expect(tree.files).toContain("/my-workspace/.gitignore"); + expect(tree.files).toContain("/my-workspace/nanoforge.config.ts"); + expect(tree.files).toContain("/my-workspace/README.md"); + expect(tree.files).not.toContain("/my-workspace/pnpm-workspace.yaml"); + }); + + it("does not generate any lint tooling files or the prettier/eslint dependencies", () => { + expect(tree.files).not.toContain("/my-workspace/eslint.config.js"); + expect(tree.files).not.toContain("/my-workspace/prettier.config.js"); + expect(tree.files).not.toContain("/my-workspace/.prettierignore"); + expect(packageJson.devDependencies["prettier"]).toBeUndefined(); + expect(packageJson.devDependencies["eslint"]).toBeUndefined(); + expect(packageJson.scripts.lint).toBeUndefined(); + expect(packageJson.scripts.format).toBeUndefined(); + }); + + it("should set the workspace name in package.json", () => { + expect(packageJson.name).toBe("my-workspace"); + }); + + it("should declare npm workspaces in package.json (apps only, no libs)", () => { + expect(packageJson.workspaces).toEqual(["apps/*"]); + }); + + it("does not include @nanoforge-dev/core", () => { + expect(packageJson.devDependencies["@nanoforge-dev/core"]).toBeUndefined(); + }); + + it("should not include pnpm config by default", () => { + expect(packageJson).not.toHaveProperty("pnpm"); + }); + + it("should resolve real versions for cli/nanoforge/typescript", () => { + expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION); + expect(packageJson.devDependencies["nanoforge"]).toMatch(RESOLVED_VERSION); + expect(packageJson.devDependencies["typescript"]).toMatch(RESOLVED_VERSION); + }); + + it("resolves typescript to a caret range (or the exact fallback version)", () => { + expect(packageJson.devDependencies["typescript"]).toMatch(/^\^?\d+\.\d+\.\d+$/); + }); + + it("does not jump typescript to its major-7 native rewrite", () => { + const version = packageJson.devDependencies["typescript"] as string; + expect(version.replace(/^\^/, "").split(".")[0]).toBe("6"); + }); + + it("should declare a workspace-typed nanoforge.config.ts with apps only (no libs)", () => { + const content = tree.readContent("/my-workspace/nanoforge.config.ts"); + expect(content).toContain('type: "workspace"'); + expect(content).toContain('packages: ["apps/*"]'); + }); + + it("does not set a packageManager (Corepack) field", () => { + expect(packageJson.packageManager).toBeUndefined(); + }); + }); + + describe("with JavaScript", () => { + let tree: UnitTestTree; + + beforeAll(async () => { + tree = await runner.runSchematic("workspace", { + name: "js-workspace", + language: "js", + }); + }); + + it("should generate JS workspace files", () => { + expect(tree.files).toContain("/js-workspace/package.json"); + expect(tree.files).toContain("/js-workspace/jsconfig.json"); + expect(tree.files).toContain("/js-workspace/nanoforge.config.js"); + expect(tree.files).not.toContain("/js-workspace/tsconfig.json"); + expect(tree.files).not.toContain("/js-workspace/nanoforge.config.ts"); + }); + + it("has the same package.json shape as the TypeScript workspace, minus typescript itself", () => { + const packageJson = JSON.parse(tree.readContent("/js-workspace/package.json")); + expect(packageJson.devDependencies["typescript"]).toBeUndefined(); + expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION); + expect(packageJson.devDependencies["@nanoforge-dev/core"]).toBeUndefined(); + expect(packageJson.devDependencies["prettier"]).toBeUndefined(); + expect(packageJson.version).toBeUndefined(); + expect(packageJson.author).toBeUndefined(); + expect(packageJson.description).toBeUndefined(); + expect(packageJson).not.toHaveProperty("lint-staged"); + expect(packageJson.workspaces).toEqual(["apps/*"]); + }); + + it("does not generate any lint tooling files (same as TypeScript)", () => { + expect(tree.files).not.toContain("/js-workspace/eslint.config.js"); + expect(tree.files).not.toContain("/js-workspace/prettier.config.js"); + expect(tree.files).not.toContain("/js-workspace/.prettierignore"); + }); + }); + + describe("with custom directory", () => { + it("should generate files in the specified directory", async () => { + const tree = await runner.runSchematic("workspace", { + name: "my-workspace", + directory: "custom-dir", + }); + expect(tree.files).toContain("/custom-dir/package.json"); + }); + }); + + describe("with pnpm", () => { + it("should generate pnpm-specific files and no npm workspaces field", async () => { + const tree = await runner.runSchematic("workspace", { + name: "pnpm-workspace", + packageManager: "pnpm", + }); + expect(tree.files).toContain("/pnpm-workspace/pnpm-workspace.yaml"); + const packageJson = JSON.parse(tree.readContent("/pnpm-workspace/package.json")); + // pnpm 10+ no longer reads the "pnpm" field from package.json (it warns and + // ignores it); build-script approval now lives in pnpm-workspace.yaml's + // `allowBuilds` map instead. See https://pnpm.io/settings. + expect(packageJson).not.toHaveProperty("pnpm"); + expect(packageJson.workspaces).toBeUndefined(); + expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION); + }); + + it("should generate a pnpm-workspace.yaml with apps only (no libs)", async () => { + const tree = await runner.runSchematic("workspace", { + name: "pnpm-packages-workspace", + packageManager: "pnpm", + }); + const pnpmWorkspaceYaml = tree.readContent("/pnpm-packages-workspace/pnpm-workspace.yaml"); + expect(pnpmWorkspaceYaml).toContain('- "apps/*"'); + expect(pnpmWorkspaceYaml).not.toContain("libs/*"); + }); + + it("always allows bun's build script in pnpm-workspace.yaml's allowBuilds map", async () => { + const tree = await runner.runSchematic("workspace", { + name: "pnpm-default-allow-builds-workspace", + packageManager: "pnpm", + }); + const pnpmWorkspaceYaml = tree.readContent( + "/pnpm-default-allow-builds-workspace/pnpm-workspace.yaml", + ); + expect(pnpmWorkspaceYaml).toContain("allowBuilds:"); + expect(pnpmWorkspaceYaml).toContain("bun: true"); + }); + + it("adds user-requested packages to pnpm-workspace.yaml's allowBuilds map alongside bun", async () => { + const tree = await runner.runSchematic("workspace", { + name: "pnpm-allow-builds-workspace", + packageManager: "pnpm", + allowBuilds: ["esbuild"], + }); + const pnpmWorkspaceYaml = tree.readContent( + "/pnpm-allow-builds-workspace/pnpm-workspace.yaml", + ); + expect(pnpmWorkspaceYaml).toContain("bun: true"); + expect(pnpmWorkspaceYaml).toContain("esbuild: true"); + }); + }); + + describe("with npm", () => { + it("always allows bun's build script via package.json's allowScripts", async () => { + const tree = await runner.runSchematic("workspace", { + name: "npm-allow-scripts-workspace", + packageManager: "npm", + allowBuilds: ["esbuild"], + }); + expect(tree.files).not.toContain("/npm-allow-scripts-workspace/pnpm-workspace.yaml"); + const packageJson = JSON.parse(tree.readContent("/npm-allow-scripts-workspace/package.json")); + expect(packageJson.allowScripts).toEqual({ bun: true, esbuild: true }); + }); + }); + + describe("with bun", () => { + it("always allows bun's build script via package.json's trustedDependencies", async () => { + const tree = await runner.runSchematic("workspace", { + name: "bun-trusted-deps-workspace", + packageManager: "bun", + allowBuilds: ["esbuild"], + }); + expect(tree.files).not.toContain("/bun-trusted-deps-workspace/pnpm-workspace.yaml"); + const packageJson = JSON.parse(tree.readContent("/bun-trusted-deps-workspace/package.json")); + expect(packageJson.trustedDependencies).toEqual(["bun", "esbuild"]); + }); + }); + + describe("with yarn", () => { + it("should not generate pnpm-workspace.yaml, no script-allow field, and pinned versions", async () => { + const tree = await runner.runSchematic("workspace", { + name: "yarn-workspace", + packageManager: "yarn", + }); + expect(tree.files).not.toContain("/yarn-workspace/pnpm-workspace.yaml"); + const packageJson = JSON.parse(tree.readContent("/yarn-workspace/package.json")); + expect(packageJson).not.toHaveProperty("allowScripts"); + expect(packageJson).not.toHaveProperty("trustedDependencies"); + expect(packageJson.workspaces).toEqual(["apps/*"]); + expect(packageJson.devDependencies["@nanoforge-dev/cli"]).toMatch(RESOLVED_VERSION); + }); + }); + + describe("with docker disabled (default)", () => { + it("should not generate a Dockerfile or .dockerignore", async () => { + const tree = await runner.runSchematic("workspace", { name: "no-docker-workspace" }); + expect(tree.files).not.toContain("/no-docker-workspace/Dockerfile"); + expect(tree.files).not.toContain("/no-docker-workspace/.dockerignore"); + }); + }); + + describe("with docker enabled", () => { + it("should generate Dockerfile and .dockerignore", async () => { + const tree = await runner.runSchematic("workspace", { + name: "docker-workspace", + docker: true, + }); + expect(tree.files).toContain("/docker-workspace/Dockerfile"); + expect(tree.files).toContain("/docker-workspace/.dockerignore"); + }); + + describe.each(["npm", "yarn", "pnpm", "bun"] as const)( + "with %s package manager", + (packageManager) => { + it("should branch Dockerfile content on the package manager", async () => { + const tree = await runner.runSchematic("workspace", { + name: `${packageManager}-docker-workspace`, + docker: true, + packageManager, + }); + const content = tree.readContent(`/${packageManager}-docker-workspace/Dockerfile`); + const installCommand = packageManager === "npm" ? "npm ci" : `${packageManager} install`; + expect(content).toContain(`RUN ${installCommand}`); + expect(content).toContain(`RUN ${packageManager} run build`); + expect(content).toContain(`CMD ["${packageManager}", "run", "start"]`); + }); + }, + ); + }); +}); diff --git a/libs/schematics/eslint.config.js b/libs/schematics/eslint.config.js new file mode 100644 index 0000000..5e9532d --- /dev/null +++ b/libs/schematics/eslint.config.js @@ -0,0 +1,3 @@ +import eslintConfig from "@nanoforge-dev/utils-eslint-config"; + +export default [...eslintConfig, { ignores: ["src/libs/**/files/**/*"] }]; diff --git a/libs/schematics/package.json b/libs/schematics/package.json new file mode 100644 index 0000000..4be5c5f --- /dev/null +++ b/libs/schematics/package.json @@ -0,0 +1,109 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@nanoforge-dev/schematics", + "version": "2.2.0", + "description": "NanoForge Schematics", + "keywords": [ + "nanoforge", + "game", + "schematics" + ], + "homepage": "https://github.com/NanoForge-dev/CLI#readme", + "bugs": "https://github.com/NanoForge-dev/CLI/issues", + "license": "MIT", + "contributors": [ + "Exelo ", + "Fexkoser ", + "Harper ", + "Josephine ", + "Leo " + ], + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./package.json": "./package.json", + "./collection.json": "./dist/collection.json" + }, + "type": "module", + "directories": { + "lib": "src" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NanoForge-dev/CLI.git", + "directory": "libs/schematics" + }, + "funding": "https://github.com/NanoForge-dev/CLI?sponsor", + "scripts": { + "build": "tsc --noEmit && tsdown", + "postbuild": "pnpm run copy:collection && pnpm run copy:lib", + "copy:collection": "cpx src/collection.json dist && cpx 'src/libs/**/schema.json' dist/libs", + "copy:lib": "cpx 'src/libs/**/files/**/{.,}*' dist/libs", + "lint": "prettier --check . && eslint --format=pretty src", + "format": "prettier --write . && eslint --fix --format=pretty src", + "prepack": "pnpm run build && pnpm run lint", + "changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'libs/schematics/*'", + "release": "cliff-jumper", + "test": "pnpm run test:unit && pnpm run test:e2e", + "test:unit": "vitest run -c vitest.config.ts", + "test:unit:watch": "vitest -c vitest.config.ts", + "test:e2e": "vitest run -c vitest.config.e2e.ts", + "test:e2e:watch": "vitest -c vitest.config.e2e.ts", + "test:coverage": "pnpm run test:unit --coverage && pnpm run test:e2e --coverage" + }, + "dependencies": { + "@angular-devkit/core": "catalog:schematics", + "@angular-devkit/schematics": "catalog:schematics", + "rxjs": "catalog:schematics" + }, + "devDependencies": { + "@angular-devkit/schematics-cli": "catalog:schematics", + "@commitlint/cli": "catalog:ci", + "@commitlint/config-conventional": "catalog:ci", + "@favware/cliff-jumper": "catalog:ci", + "@nanoforge-dev/actions": "catalog:ci", + "@nanoforge-dev/utils-eslint-config": "catalog:lint", + "@nanoforge-dev/utils-prettier-config": "catalog:lint", + "@trivago/prettier-plugin-sort-imports": "catalog:lint", + "@types/node": "catalog:core", + "@vitest/coverage-v8": "catalog:test", + "cpx2": "catalog:build", + "eslint": "catalog:lint", + "husky": "catalog:ci", + "lint-staged": "catalog:ci", + "prettier": "catalog:lint", + "tsdown": "catalog:build", + "typescript": "catalog:build", + "vitest": "catalog:test" + }, + "packageManager": "pnpm@12.0.0", + "engines": { + "node": "26" + }, + "publishConfig": { + "access": "public" + }, + "schematics": "./dist/collection.json", + "lint-staged": { + "**": [ + "prettier --ignore-unknown --write" + ], + "src/**/*.ts": [ + "eslint --fix" + ] + } +} diff --git a/libs/schematics/prettier.config.js b/libs/schematics/prettier.config.js new file mode 100644 index 0000000..121c003 --- /dev/null +++ b/libs/schematics/prettier.config.js @@ -0,0 +1,6 @@ +import prettierConfig from "@nanoforge-dev/utils-prettier-config"; + +export default { + ...prettierConfig, + importOrder: ["^@utils/(.*)$", "^~/(.*)$", "^[./]"], +}; diff --git a/libs/schematics/src/collection.json b/libs/schematics/src/collection.json new file mode 100644 index 0000000..3fdca2d --- /dev/null +++ b/libs/schematics/src/collection.json @@ -0,0 +1,25 @@ +{ + "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "workspace": { + "factory": "./libs/workspace/workspace.factory#main", + "description": "Create a NanoForge workspace (monorepo root: config, tooling, package manifest).", + "schema": "./libs/workspace/schema.json" + }, + "project": { + "factory": "./libs/project/project.factory#main", + "description": "Create a NanoForge client or server project under apps/.", + "schema": "./libs/project/schema.json" + }, + "component": { + "factory": "./libs/component/component.factory#main", + "description": "Create a NanoForge component.", + "schema": "./libs/component/schema.json" + }, + "system": { + "factory": "./libs/system/system.factory#main", + "description": "Create a NanoForge system.", + "schema": "./libs/system/schema.json" + } + } +} diff --git a/libs/schematics/src/defaults.ts b/libs/schematics/src/defaults.ts new file mode 100644 index 0000000..f4621c9 --- /dev/null +++ b/libs/schematics/src/defaults.ts @@ -0,0 +1,22 @@ +export const DEFAULT_APP_NAME = "nanoforge-app"; +export const DEFAULT_LANGUAGE = "ts"; +export const DEFAULT_PACKAGE_MANAGER = "npm"; +export const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = "^2"; +export const DEFAULT_ENGINE_VERSION = "^2"; + +/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */ +export const DEFAULT_CLI_DEPENDENCY_VERSION = "latest"; + +/** + * Fallback used when the registry lookup for `typescript` fails. A specific + * version, not a range — non-nanoforge packages are pinned exactly. + */ +export const DEFAULT_TYPESCRIPT_VERSION = "6.0.3"; + +/** + * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when + * resolving a pinned dependency version at generation time, only consider + * versions published at least this long ago, so a freshly (and potentially + * maliciously) published release isn't picked up immediately. + */ +export const MIN_RELEASE_AGE_HOURS = 48; diff --git a/libs/schematics/src/index.ts b/libs/schematics/src/index.ts new file mode 100644 index 0000000..8156f2d --- /dev/null +++ b/libs/schematics/src/index.ts @@ -0,0 +1 @@ +export * from "./defaults"; diff --git a/libs/schematics/src/libs/component/component.factory.ts b/libs/schematics/src/libs/component/component.factory.ts new file mode 100644 index 0000000..da8ba33 --- /dev/null +++ b/libs/schematics/src/libs/component/component.factory.ts @@ -0,0 +1,41 @@ +import { type Path, join, normalize, strings } from "@angular-devkit/core"; +import { + type Rule, + type Source, + apply, + mergeWith, + move, + template, + url, +} from "@angular-devkit/schematics"; + +import { toKebabCase, toPascalCase } from "@utils/formatting"; + +import { type ComponentOptions } from "./component.options"; +import { type ComponentSchema } from "./component.schema"; + +const transform = (schema: ComponentSchema): ComponentOptions => { + return { + part: schema.part, + className: toPascalCase(schema.name), + fileName: toKebabCase(schema.name), + }; +}; + +const generate = (options: ComponentOptions, path: string, language: "ts" | "js"): Source => { + const rules = [ + template({ + ...strings, + ...options, + }), + move(normalize(path)), + ]; + + return apply(url(join("./files" as Path, language)), rules); +}; + +export const main = (schema: ComponentSchema): Rule => { + const options = transform(schema); + + return mergeWith(generate(options, schema.directory, schema.language)); +}; diff --git a/libs/schematics/src/libs/component/component.options.d.ts b/libs/schematics/src/libs/component/component.options.d.ts new file mode 100644 index 0000000..270628d --- /dev/null +++ b/libs/schematics/src/libs/component/component.options.d.ts @@ -0,0 +1,16 @@ +export interface ComponentOptions { + /** + * The part of the application to generate + */ + part: "client" | "server"; + + /** + * Component class name + */ + className: string; + + /** + * Component filename + */ + fileName: string; +} diff --git a/libs/schematics/src/libs/component/component.schema.d.ts b/libs/schematics/src/libs/component/component.schema.d.ts new file mode 100644 index 0000000..4285128 --- /dev/null +++ b/libs/schematics/src/libs/component/component.schema.d.ts @@ -0,0 +1,21 @@ +export interface ComponentSchema { + /** + * Component destination directory + */ + name: string; + + /** + * Component destination directory + */ + directory: string; + + /** + * The part of the application to generate + */ + part: "client" | "server"; + + /** + * NanoForge Application language + */ + language: "js" | "ts"; +} diff --git a/libs/schematics/src/libs/component/files/js/__fileName__.component.js b/libs/schematics/src/libs/component/files/js/__fileName__.component.js new file mode 100644 index 0000000..10ad6b3 --- /dev/null +++ b/libs/schematics/src/libs/component/files/js/__fileName__.component.js @@ -0,0 +1,72 @@ +/** + * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorComponentManifest} EditorComponentManifest + */ + +export class <%= className %>Component { + static name = "<%= className %>Component"; + name = this.constructor.name; + + paramA; + paramB; + paramC; + + /** + * <%= className %> component constructor + * @param {string} paramA + * @param {number} paramB + * @param {boolean} [paramC = false] + */ + constructor(paramA, paramB, paramC = false) { + this.paramA = paramA; + this.paramB = paramB; + this.paramC = paramC; + } + + get foo() { + return "bar"; + } + + get paramAOrDefault() { + return this.paramC ? this.paramA : "default"; + } + + addOne() { + this.paramB += 1; + } +} + +// * Required to generate code +export default <%= className %>Component.name; + +/** + * Editor component manifest + * * Required for the editor to display the component and generate code + * @type {EditorComponentManifest} + */ +export const EDITOR_COMPONENT_MANIFEST = { + name: "<%= className %>", + description: "<%= className %> component description", + params: [ + { + type: "string", + name: "paramA", + description: "Param A description", + example: "Example value", + }, + { + type: "number", + name: "paramB", + description: "Param B description", + example: 3, + }, + { + type: "boolean", + name: "paramC", + description: "Param C description", + example: true, + default: false, + // Not required because it has a default value + optional: true, + }, + ], +}; diff --git a/libs/schematics/src/libs/component/files/ts/__fileName__.component.ts b/libs/schematics/src/libs/component/files/ts/__fileName__.component.ts new file mode 100644 index 0000000..2be836f --- /dev/null +++ b/libs/schematics/src/libs/component/files/ts/__fileName__.component.ts @@ -0,0 +1,55 @@ +import { type EditorComponentManifest } from "@nanoforge-dev/ecs-<%= part %>"; + +export class <%= className %>Component { + name = this.constructor.name; + + constructor( + public paramA: string, + public paramB: number, + public paramC: boolean = false, + ) {} + + get foo() { + return "bar"; + } + + get paramAOrDefault() { + return this.paramC ? this.paramA : "default"; + } + + addOne() { + this.paramB += 1; + } +} + +// * Required to generate code +export default <%= className %>Component.name; + +// * Required for the editor to display the component and generate code +export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = { + name: "<%= className %>", + description: "<%= className %> component description", + params: [ + { + type: "string", + name: "paramA", + description: "Param A description", + example: "Example value", + }, + { + type: "number", + name: "paramB", + description: "Param B description", + example: 3, + }, + { + type: "boolean", + name: "paramC", + description: "Param C description", + example: true, + default: false, + // Not required because it has a default value + optional: true, + }, + ], +}; diff --git a/libs/schematics/src/libs/component/schema.json b/libs/schematics/src/libs/component/schema.json new file mode 100644 index 0000000..a5a6d7f --- /dev/null +++ b/libs/schematics/src/libs/component/schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsNanoForgeComponent", + "title": "NanoForge Component Options Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Component name", + "default": "example" + }, + "directory": { + "type": "string", + "description": "Component destination directory", + "default": "." + }, + "part": { + "type": "string", + "enum": ["client", "server"], + "description": "The part of the application to generate" + }, + "language": { + "type": "string", + "enum": ["ts", "js"], + "description": "NanoForge application language", + "default": "ts" + } + }, + "required": ["part"] +} diff --git a/libs/schematics/src/libs/project/files/js/.dockerignore b/libs/schematics/src/libs/project/files/js/.dockerignore new file mode 100644 index 0000000..6769f3a --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.nanoforge/ +.git/ +.env diff --git a/libs/schematics/src/libs/project/files/js/Dockerfile b/libs/schematics/src/libs/project/files/js/Dockerfile new file mode 100644 index 0000000..7b95dad --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/Dockerfile @@ -0,0 +1,36 @@ +<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base +<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base +RUN pnpm runtime set node 26 -g +<% } else { %>FROM node:26-slim AS base +<% } %><% if (packageManager === "yarn") { %> +RUN npm install -g yarn +<% } %> +WORKDIR /app + +FROM base AS deps + +<% if (packageManager === "npm") { %>COPY package.json package-lock.json ./ +RUN npm ci +<% } else if (packageManager === "yarn") { %>COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +<% } else if (packageManager === "pnpm") { %>COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +<% } else { %>COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile +<% } %> +FROM deps AS build +COPY . . +RUN <%= packageManager %> run build + +<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %> +WORKDIR /app + +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/package.json ./ +COPY --from=build /app/nanoforge.config.js ./ +<% if (part === "server") { %> +<% } %><% if (packageManager !== "pnpm") { %> +USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %> +<% } %><% if (part === "client") { %>EXPOSE 3000 +<% } %>CMD ["<%= packageManager %>", "run", "start"] diff --git a/libs/schematics/src/libs/project/files/js/README.md b/libs/schematics/src/libs/project/files/js/README.md new file mode 100644 index 0000000..b068141 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/README.md @@ -0,0 +1,53 @@ +# <%= packageName %> + +A [NanoForge](https://nanoforge.eu) <%= part %> app. +<% if (!workspace) { %> +## Prerequisites + +- Node.js and <%= packageManager %> +- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli` + +## Install + +```bash +<%= packageManager %> install +``` +<% } %> +## Development + +```bash +nf dev +``` + +`nf dev` serves the game and watches your files, so changes are picked up as you work. + +## Build & start + +```bash +nf build +nf start +``` + +`nf build` compiles this <%= part %> for production, and `nf start` serves the build. +<% if (editor) { %> +This app was generated with editor support. Launch it with: + +```bash +nf editor . +``` +<% } %><% if (docker) { %> +A `Dockerfile` is also included to containerize the built app for deployment. +<% } %> +## CLI reference + +| Command | Description | +| ------------- | ---------------------------------- | +| `nf dev` | Develop with hot reload | +| `nf build` | Build for production | +| `nf start` | Run a built app |<% if (editor) { %> +| `nf editor` | Launch the visual editor |<% } %> + +## More info + +- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides +- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge diff --git a/libs/schematics/src/libs/project/files/js/__dot__gitignore b/libs/schematics/src/libs/project/files/js/__dot__gitignore new file mode 100644 index 0000000..ba6da87 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/__dot__gitignore @@ -0,0 +1,134 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# File-based project format +*.iws + +# Editor-based Rest Client +.idea/httpRequests + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### NanoForge template +# Compilation directories +.nanoforge/client +.nanoforge/server diff --git a/libs/schematics/src/libs/project/files/js/assets/__dot__gitkeep b/libs/schematics/src/libs/project/files/js/assets/__dot__gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/schematics/src/libs/project/files/js/jsconfig.json b/libs/schematics/src/libs/project/files/js/jsconfig.json new file mode 100644 index 0000000..5653881 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/jsconfig.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json.schemastore.org/jsconfig.json",<% if (workspace) { %> + "extends": "../../jsconfig.json",<% } else { %> + "compilerOptions": { + "allowUnreachableCode": <%= !strict %>, + "allowUnusedLabels": <%= !strict %>, + "exactOptionalPropertyTypes": <%= strict %>, + "noFallthroughCasesInSwitch": <%= strict %>, + "noImplicitOverride": <%= strict %>, + "noImplicitReturns": <%= strict %>, + "noPropertyAccessFromIndexSignature": <%= !strict %>, + "noUncheckedIndexedAccess": <%= strict %>, + "noUnusedLocals": <%= strict %>, + "noUnusedParameters": <%= strict %>, + + "allowArbitraryExtensions": false, + "allowImportingTsExtensions": false, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + + "experimentalDecorators": true, + "lib": ["ESNext", "esnext.disposable", "DOM"], + "target": "ESNext", + "useDefineForClassFields": true + },<% } %> + "include": ["src"], + "exclude": ["node_modules", ".nanoforge", "dist"] +} diff --git a/libs/schematics/src/libs/project/files/js/nanoforge.config.js b/libs/schematics/src/libs/project/files/js/nanoforge.config.js new file mode 100644 index 0000000..b42a8a5 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/nanoforge.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "nanoforge/config"; + +export default defineConfig({ + type: "<%= part %>", + entryFile: "src/main.js", + editor: { + entryFile: "src/main.js", + }, + language: "js", +}); diff --git a/libs/schematics/src/libs/project/files/js/package.json.template b/libs/schematics/src/libs/project/files/js/package.json.template new file mode 100644 index 0000000..9124d89 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/package.json.template @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "<%= packageName %>", + "scripts": { + "dev": "nf dev", + "build": "nf build", + "start": "nf start" + },<% if (part === "server") { %> + "dependencies": { + "@nanoforge-dev/network": "<%= networkVersion %>" + },<% } %> + "devDependencies": { + "@nanoforge-dev/ecs": "<%= ecsVersion %>",<% if (part === "client") { %> + "@nanoforge-dev/graphics-2d": "<%= graphics2dVersion %>", + "@nanoforge-dev/input": "<%= inputVersion %>", + "@nanoforge-dev/network": "<%= networkVersion %>",<% } %> + "nanoforge": "<%= nanoforgeVersion %>" + },<% if (packageManager === "npm" && allowBuilds.length > 0) { %> + "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %> + "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %> + },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %> + "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %> + "private": true +} diff --git a/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template new file mode 100644 index 0000000..1e7271b --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/pnpm-workspace.yaml.template @@ -0,0 +1,3 @@ +<% if (allowBuilds.length > 0) { %>allowBuilds:<% allowBuilds.forEach(function (pkg) { %> + <%= pkg %>: true<% }); %> +<% } %> \ No newline at end of file diff --git a/libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js b/libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js new file mode 100644 index 0000000..2f3a3f5 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/src/components/drawable-circle-2d.component.js @@ -0,0 +1,404 @@ +/** + * @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest + * @typedef {import("@nanoforge-dev/graphics-2d").Layer} Layer + * @typedef {import("nanoforge").NfFile} NfFile + */ + +import { Circle } from "@nanoforge-dev/graphics-2d"; + +export class DrawableCircle2D { + name = this.constructor.name; + shape; + + /** @type {NfFile | undefined} */ + #fillPatternImage; + + /** + * @param {number} [radius] + * @param {boolean} [visible] + * @param {string} [id] + * @param {number} [opacity] + * @param {number} [scaleX] + * @param {number} [scaleY] + * @param {number} [skewX] + * @param {number} [skewY] + * @param {number} [rotation] + * @param {number} [offsetX] + * @param {number} [offsetY] + * @param {string} [fillColor] + * @param {NfFile} [fillPatternImage] + * @param {number} [fillPatternX] + * @param {number} [fillPatternY] + * @param {number} [fillPatternOffsetX] + * @param {number} [fillPatternOffsetY] + * @param {number} [fillPatternScaleX] + * @param {number} [fillPatternScaleY] + * @param {number} [fillPatternRotation] + * @param {string} [fillPatternRepeat] + */ + constructor( + radius, + visible, + id, + opacity, + scaleX, + scaleY, + skewX, + skewY, + rotation, + offsetX, + offsetY, + fillColor, + fillPatternImage, + fillPatternX, + fillPatternY, + fillPatternOffsetX, + fillPatternOffsetY, + fillPatternScaleX, + fillPatternScaleY, + fillPatternRotation, + fillPatternRepeat, + ) { + this.#fillPatternImage = fillPatternImage; + + this.shape = new Circle({ + radius, + visible, + id, + opacity, + scaleX, + scaleY, + skewX, + skewY, + rotation, + offsetX, + offsetY, + fill: fillColor, + fillPatternImage: fillPatternImage + ? Object.assign(new Image(), { src: fillPatternImage.path }) + : undefined, + fillPatternX, + fillPatternY, + fillPatternOffsetX, + fillPatternOffsetY, + fillPatternScaleX, + fillPatternScaleY, + fillPatternRotation, + fillPatternRepeat, + }); + } + + get radius() { + return this.shape.radius(); + } + set radius(v) { + this.shape.radius(v); + this.#redraw(); + } + get visible() { + return this.shape.visible(); + } + set visible(v) { + this.shape.visible(v); + this.#redraw(); + } + get id() { + return this.shape.id(); + } + set id(v) { + this.shape.id(v); + this.#redraw(); + } + get opacity() { + return this.shape.opacity(); + } + set opacity(v) { + this.shape.opacity(v); + this.#redraw(); + } + get scaleX() { + return this.shape.scaleX(); + } + set scaleX(v) { + this.shape.scaleX(v); + this.#redraw(); + } + get scaleY() { + return this.shape.scaleY(); + } + set scaleY(v) { + this.shape.scaleY(v); + this.#redraw(); + } + get skewX() { + return this.shape.skewX(); + } + set skewX(v) { + this.shape.skewX(v); + this.#redraw(); + } + get skewY() { + return this.shape.skewY(); + } + set skewY(v) { + this.shape.skewY(v); + this.#redraw(); + } + get rotation() { + return this.shape.rotation(); + } + set rotation(v) { + this.shape.rotation(v); + this.#redraw(); + } + get offsetX() { + return this.shape.offsetX(); + } + set offsetX(v) { + this.shape.offsetX(v); + this.#redraw(); + } + get offsetY() { + return this.shape.offsetY(); + } + set offsetY(v) { + this.shape.offsetY(v); + this.#redraw(); + } + get fillColor() { + return this.shape.fill().toString(); + } + set fillColor(v) { + this.shape.fill(v); + this.#redraw(); + } + /** @returns {NfFile | undefined} */ + get fillPatternImage() { + return this.#fillPatternImage; + } + /** @param {NfFile} v */ + set fillPatternImage(v) { + this.#fillPatternImage = v; + this.shape.fillPatternImage(v ? Object.assign(new Image(), { src: v.path }) : undefined); + this.#redraw(); + } + get fillPatternX() { + return this.shape.fillPatternX(); + } + set fillPatternX(v) { + this.shape.fillPatternX(v); + this.#redraw(); + } + get fillPatternY() { + return this.shape.fillPatternY(); + } + set fillPatternY(v) { + this.shape.fillPatternY(v); + this.#redraw(); + } + get fillPatternOffsetX() { + return this.shape.fillPatternOffsetX(); + } + set fillPatternOffsetX(v) { + this.shape.fillPatternOffsetX(v); + this.#redraw(); + } + get fillPatternOffsetY() { + return this.shape.fillPatternOffsetY(); + } + set fillPatternOffsetY(v) { + this.shape.fillPatternOffsetY(v); + this.#redraw(); + } + get fillPatternScaleX() { + return this.shape.fillPatternScaleX(); + } + set fillPatternScaleX(v) { + this.shape.fillPatternScaleX(v); + this.#redraw(); + } + get fillPatternScaleY() { + return this.shape.fillPatternScaleY(); + } + set fillPatternScaleY(v) { + this.shape.fillPatternScaleY(v); + this.#redraw(); + } + get fillPatternRotation() { + return this.shape.fillPatternRotation(); + } + set fillPatternRotation(v) { + this.shape.fillPatternRotation(v); + this.#redraw(); + } + get fillPatternRepeat() { + return this.shape.fillPatternRepeat(); + } + set fillPatternRepeat(v) { + this.shape.fillPatternRepeat(v); + this.#redraw(); + } + + /** @param {Layer} layer */ + addToLayer(layer) { + if (this.shape.getParent() !== layer) layer.add(this.shape); + } + + #redraw() { + this.shape.getLayer()?.batchDraw(); + } +} +// * Required to generate code +export default DrawableCircle2D.name; + +// * Required for the editor to display the component and generate code +/** @type {EditorComponentManifest} */ +export const EDITOR_COMPONENT_MANIFEST = { + name: "DrawableCircle2D", + description: "Graphically drawable circle 2D", + params: [ + { + name: "radius", + type: "number", + description: "Radius of the circle in pixels", + default: 10, + optional: false, + }, + { + name: "visible", + type: "boolean", + description: "Controls whether the node is rendered", + default: true, + optional: false, + }, + { + name: "id", + type: "string", + description: "Unique identifier for the node, used with stage.findOne('#id')", + optional: true, + }, + { + name: "opacity", + type: "number", + description: "Opacity from 0 (transparent) to 1 (fully opaque)", + default: 1, + optional: true, + }, + { + name: "scaleX", + type: "number", + description: "Horizontal scale factor", + default: 1, + optional: true, + }, + { + name: "scaleY", + type: "number", + description: "Vertical scale factor", + default: 1, + optional: true, + }, + { + name: "skewX", + type: "number", + default: 0, + description: "Horizontal skew in radians, shears along the X axis", + optional: true, + }, + { + name: "skewY", + type: "number", + default: 0, + description: "Vertical skew in radians, shears along the Y axis", + optional: true, + }, + { + name: "rotation", + type: "number", + description: + "Rotation angle in degrees applied around the offset point (positive values rotate clockwise)", + default: 0, + optional: true, + }, + { + name: "offsetX", + type: "number", + description: "Horizontal offset for the transform origin", + default: 0, + optional: true, + }, + { + name: "offsetY", + type: "number", + description: "Vertical offset for the transform origin", + default: 0, + optional: true, + }, + { + name: "fillColor", + type: "string", + description: + 'Solid fill color as a CSS color string (e.g. "red", "#ff0000", "rgba(255,0,0,0.5)"), or a pre-built CanvasGradient object', + default: "red", + example: "rgb(255,255,255)", + optional: true, + }, + { + name: "fillPatternImage", + type: "asset", + description: "Image element to use as a tiling pattern fill", + optional: true, + }, + { + name: "fillPatternX", + type: "number", + description: + "Horizontal offset of the pattern origin relative to the shape's top-left corner, in pixels", + optional: true, + }, + { + name: "fillPatternY", + type: "number", + description: + "Vertical offset of the pattern origin relative to the shape's top-left corner, in pixels", + optional: true, + }, + { + name: "fillPatternOffsetX", + type: "number", + description: "Horizontally shifts the pattern image within the tile before repeating", + optional: true, + }, + { + name: "fillPatternOffsetY", + type: "number", + description: "Vertically shifts the pattern image within the tile before repeating", + optional: true, + }, + { + name: "fillPatternScaleX", + type: "number", + description: "Horizontal scale factor for the pattern tile", + optional: true, + }, + { + name: "fillPatternScaleY", + type: "number", + description: "Vertical scale factor for the pattern tile", + optional: true, + }, + { + name: "fillPatternRotation", + type: "number", + description: "Rotation of the pattern tile in degrees", + optional: true, + }, + { + name: "fillPatternRepeat", + type: "string", + description: + 'Repeat mode for the pattern. One of "repeat" (default), "repeat-x", "repeat-y", or "no-repeat"', + optional: true, + }, + ], +}; diff --git a/libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js b/libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js new file mode 100644 index 0000000..82f4b4f --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/src/components/position-2d.component.js @@ -0,0 +1,42 @@ +/** + * @typedef {import("@nanoforge-dev/ecs").EditorComponentManifest} EditorComponentManifest + */ + +export class Position2D { + name = this.constructor.name; + + /** + * @param {number} x + * @param {number} y + */ + constructor(x, y) { + this.x = x; + this.y = y; + } +} + +// * Required to generate code +export default Position2D.name; + +// * Required for the editor to display the component and generate code +/** @type {EditorComponentManifest} */ +export const EDITOR_COMPONENT_MANIFEST = { + name: "Position2D", + description: "Position of an entity in a 2 dimensional space in pixel", + params: [ + { + type: "number", + name: "x", + description: "Horizontal position in pixel", + example: 4.2, + default: 0, + }, + { + type: "number", + name: "y", + description: "Vertical position in pixel", + example: 67, + default: 0, + }, + ], +}; diff --git a/libs/schematics/src/libs/project/files/js/src/main.js b/libs/schematics/src/libs/project/files/js/src/main.js new file mode 100644 index 0000000..68ec50a --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/src/main.js @@ -0,0 +1,43 @@ +import { NanoforgeFactory } from "nanoforge"; +import { EcsLibrary } from "@nanoforge-dev/ecs/<%= part %>";<% if (part === "client") { %> +import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d"; +import { InputLibrary } from "@nanoforge-dev/input";<% } %> +import { <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %> } from "@nanoforge-dev/network/<%= part %>"; +<% if (part === "client") { %> +import { DrawableCircle2D } from "./components/drawable-circle-2d.component";<% } %> +import { Position2D } from "./components/position-2d.component";<% if (part === "client") { %> +import { draw2D } from "./systems/draw-2d.system";<% if (hasServer) { %> +import { positionSync } from "./systems/position-sync.system";<% } %><% } else { %> +import { move2D } from "./systems/move-2d.system";<% } %> + +/** + * @param {import("nanoforge").<%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %>} options + * @returns {Promise} + */ +export const main = async (options) => { + const app = NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>({ tickRate: 60 }); + + const ecs = new EcsLibrary(); + app.use(ecs);<% if (part === "client") { %> + app.use(new Graphics2DLibrary()); + app.use(new InputLibrary());<% } %> + app.use(new <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %>()); + + await app.init(options); + + const registry = ecs.registry; +<% if (part === "client") { %> + const circle = registry.spawnEntity(); + registry.addComponent(circle, new Position2D(500, 500)); + registry.addComponent(circle, new DrawableCircle2D(75, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, "red")); + + registry.addSystem(draw2D);<% if (hasServer) { %> + registry.addSystem(positionSync);<% } %> +<% } else { %> + const entity = registry.spawnEntity(); + registry.addComponent(entity, new Position2D(500, 500)); + + registry.addSystem(move2D); +<% } %> + await app.run(); +}; diff --git a/libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js b/libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js new file mode 100644 index 0000000..2312b4c --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/src/systems/draw-2d.system.js @@ -0,0 +1,33 @@ +/** + * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest + * @typedef {import("@nanoforge-dev/ecs").Registry} Registry + * @typedef {import("nanoforge").Context} Context + */ + +import { DrawableCircle2D } from "../components/drawable-circle-2d.component"; +import { Position2D } from "../components/position-2d.component"; + +/** + * @param {Registry} registry + * @param {Context} ctx + */ +export function draw2D(registry, ctx) { + const graphic2d = ctx.graphics; + + const entities = registry.getZipper([Position2D, DrawableCircle2D]); + + entities.forEach(({ Position2D, DrawableCircle2D }) => { + DrawableCircle2D.addToLayer(graphic2d.baseLayer); + DrawableCircle2D.shape.setPosition(Position2D); + }); +} +// * Required to generate code +export default draw2D.name; + +// * Required for the editor to display the system and generate code +/** @type {EditorSystemManifest} */ +export const EDITOR_SYSTEM_MANIFEST = { + name: "draw2D", + description: "Draw every drawable component", + dependencies: ["Position2D", "DrawableCircle2D"], +}; diff --git a/libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js b/libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js new file mode 100644 index 0000000..a619065 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/src/systems/move-2d.system.js @@ -0,0 +1,34 @@ +/** + * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest + * @typedef {import("@nanoforge-dev/ecs").Registry} Registry + * @typedef {import("nanoforge").Context} Context + */ + +import { Position2D } from "../components/position-2d.component"; + +/** + * @param {Registry} registry + * @param {Context} ctx + */ +export function move2D(registry, ctx) { + const network = ctx.network; + const entities = registry.getZipper([Position2D]); + + entities.forEach(({ Position2D }) => { + Position2D.x += 1; + + network.tcp.sendToEverybody( + new TextEncoder().encode(JSON.stringify({ x: Position2D.x, y: Position2D.y })), + ); + }); +} +// * Required to generate code +export default move2D.name; + +// * Required for the editor to display the system and generate code +/** @type {EditorSystemManifest} */ +export const EDITOR_SYSTEM_MANIFEST = { + name: "move2D", + description: "Moves the entity and broadcasts its position to every connected client", + dependencies: ["Position2D"], +}; diff --git a/libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js b/libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js new file mode 100644 index 0000000..d9339e0 --- /dev/null +++ b/libs/schematics/src/libs/project/files/js/src/systems/position-sync.system.js @@ -0,0 +1,35 @@ +/** + * @typedef {import("@nanoforge-dev/ecs").EditorSystemManifest} EditorSystemManifest + * @typedef {import("@nanoforge-dev/ecs").Registry} Registry + * @typedef {import("nanoforge").Context} Context + */ + +import { Position2D } from "../components/position-2d.component"; + +/** + * @param {Registry} registry + * @param {Context} ctx + */ +export function positionSync(registry, ctx) { + const network = ctx.network; + const packets = network.tcp + .getReceivedPackets() + .map((packet) => JSON.parse(new TextDecoder().decode(packet))); + + const [entity] = registry.getZipper([Position2D]); + + packets.forEach((packet) => { + entity.Position2D.x = packet.x; + entity.Position2D.y = packet.y; + }); +} +// * Required to generate code +export default positionSync.name; + +// * Required for the editor to display the system and generate code +/** @type {EditorSystemManifest} */ +export const EDITOR_SYSTEM_MANIFEST = { + name: "positionSync", + description: "Overwrites the local entity's position with the latest value received from the server", + dependencies: ["Position2D"], +}; diff --git a/libs/schematics/src/libs/project/files/ts/.dockerignore b/libs/schematics/src/libs/project/files/ts/.dockerignore new file mode 100644 index 0000000..6769f3a --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.nanoforge/ +.git/ +.env diff --git a/libs/schematics/src/libs/project/files/ts/Dockerfile b/libs/schematics/src/libs/project/files/ts/Dockerfile new file mode 100644 index 0000000..3f3e585 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/Dockerfile @@ -0,0 +1,36 @@ +<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base +<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base +RUN pnpm runtime set node 26 -g +<% } else { %>FROM node:26-slim AS base +<% } %><% if (packageManager === "yarn") { %> +RUN npm install -g yarn +<% } %> +WORKDIR /app + +FROM base AS deps + +<% if (packageManager === "npm") { %>COPY package.json package-lock.json ./ +RUN npm ci +<% } else if (packageManager === "yarn") { %>COPY package.json yarn.lock ./ +RUN yarn install --frozen-lockfile +<% } else if (packageManager === "pnpm") { %>COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +<% } else { %>COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile +<% } %> +FROM deps AS build +COPY . . +RUN <%= packageManager %> run build + +<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %> +WORKDIR /app + +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/package.json ./ +COPY --from=build /app/nanoforge.config.ts ./ +<% if (part === "server") { %> +<% } %><% if (packageManager !== "pnpm") { %> +USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %> +<% } %><% if (part === "client") { %>EXPOSE 3000 +<% } %>CMD ["<%= packageManager %>", "run", "start"] diff --git a/libs/schematics/src/libs/project/files/ts/README.md b/libs/schematics/src/libs/project/files/ts/README.md new file mode 100644 index 0000000..b068141 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/README.md @@ -0,0 +1,53 @@ +# <%= packageName %> + +A [NanoForge](https://nanoforge.eu) <%= part %> app. +<% if (!workspace) { %> +## Prerequisites + +- Node.js and <%= packageManager %> +- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli` + +## Install + +```bash +<%= packageManager %> install +``` +<% } %> +## Development + +```bash +nf dev +``` + +`nf dev` serves the game and watches your files, so changes are picked up as you work. + +## Build & start + +```bash +nf build +nf start +``` + +`nf build` compiles this <%= part %> for production, and `nf start` serves the build. +<% if (editor) { %> +This app was generated with editor support. Launch it with: + +```bash +nf editor . +``` +<% } %><% if (docker) { %> +A `Dockerfile` is also included to containerize the built app for deployment. +<% } %> +## CLI reference + +| Command | Description | +| ------------- | ---------------------------------- | +| `nf dev` | Develop with hot reload | +| `nf build` | Build for production | +| `nf start` | Run a built app |<% if (editor) { %> +| `nf editor` | Launch the visual editor |<% } %> + +## More info + +- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides +- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge diff --git a/libs/schematics/src/libs/project/files/ts/__dot__gitignore b/libs/schematics/src/libs/project/files/ts/__dot__gitignore new file mode 100644 index 0000000..ba6da87 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/__dot__gitignore @@ -0,0 +1,134 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# File-based project format +*.iws + +# Editor-based Rest Client +.idea/httpRequests + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### NanoForge template +# Compilation directories +.nanoforge/client +.nanoforge/server diff --git a/libs/schematics/src/libs/project/files/ts/assets/__dot__gitkeep b/libs/schematics/src/libs/project/files/ts/assets/__dot__gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/libs/schematics/src/libs/project/files/ts/nanoforge.config.ts b/libs/schematics/src/libs/project/files/ts/nanoforge.config.ts new file mode 100644 index 0000000..b05cbcb --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/nanoforge.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from "nanoforge/config"; + +export default defineConfig({ + type: "<%= part %>", +}); diff --git a/libs/schematics/src/libs/project/files/ts/package.json.template b/libs/schematics/src/libs/project/files/ts/package.json.template new file mode 100644 index 0000000..9124d89 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/package.json.template @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "<%= packageName %>", + "scripts": { + "dev": "nf dev", + "build": "nf build", + "start": "nf start" + },<% if (part === "server") { %> + "dependencies": { + "@nanoforge-dev/network": "<%= networkVersion %>" + },<% } %> + "devDependencies": { + "@nanoforge-dev/ecs": "<%= ecsVersion %>",<% if (part === "client") { %> + "@nanoforge-dev/graphics-2d": "<%= graphics2dVersion %>", + "@nanoforge-dev/input": "<%= inputVersion %>", + "@nanoforge-dev/network": "<%= networkVersion %>",<% } %> + "nanoforge": "<%= nanoforgeVersion %>" + },<% if (packageManager === "npm" && allowBuilds.length > 0) { %> + "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %> + "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %> + },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %> + "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %> + "private": true +} diff --git a/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template new file mode 100644 index 0000000..1e7271b --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/pnpm-workspace.yaml.template @@ -0,0 +1,3 @@ +<% if (allowBuilds.length > 0) { %>allowBuilds:<% allowBuilds.forEach(function (pkg) { %> + <%= pkg %>: true<% }); %> +<% } %> \ No newline at end of file diff --git a/libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts b/libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts new file mode 100644 index 0000000..30c4454 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/src/components/drawable-circle-2d.component.ts @@ -0,0 +1,367 @@ +import type { EditorComponentManifest } from "@nanoforge-dev/ecs"; +import { Circle, type Layer } from "@nanoforge-dev/graphics-2d"; +import type { NfFile } from "nanoforge"; + +export class DrawableCircle2D { + public readonly name: string = this.constructor.name; + shape: Circle; + + constructor( + radius?: number, + visible?: boolean, + id?: string, + opacity?: number, + scaleX?: number, + scaleY?: number, + skewX?: number, + skewY?: number, + rotation?: number, + offsetX?: number, + offsetY?: number, + fillColor?: string, + private _fillPatternImage?: NfFile, + fillPatternX?: number, + fillPatternY?: number, + fillPatternOffsetX?: number, + fillPatternOffsetY?: number, + fillPatternScaleX?: number, + fillPatternScaleY?: number, + fillPatternRotation?: number, + fillPatternRepeat?: string, + ) { + this.shape = new Circle({ + radius, + visible, + id, + opacity, + scaleX, + scaleY, + skewX, + skewY, + rotation, + offsetX, + offsetY, + fill: fillColor, + fillPatternImage: _fillPatternImage + ? Object.assign(new Image(), { src: _fillPatternImage.path }) + : undefined, + fillPatternX, + fillPatternY, + fillPatternOffsetX, + fillPatternOffsetY, + fillPatternScaleX, + fillPatternScaleY, + fillPatternRotation, + fillPatternRepeat, + }); + } + + get radius() { + return this.shape.radius(); + } + set radius(v: number) { + this.shape.radius(v); + this.redraw(); + } + get visible() { + return this.shape.visible(); + } + set visible(v: boolean) { + this.shape.visible(v); + this.redraw(); + } + get id() { + return this.shape.id(); + } + set id(v: string) { + this.shape.id(v); + this.redraw(); + } + get opacity() { + return this.shape.opacity(); + } + set opacity(v: number) { + this.shape.opacity(v); + this.redraw(); + } + get scaleX() { + return this.shape.scaleX(); + } + set scaleX(v: number) { + this.shape.scaleX(v); + this.redraw(); + } + get scaleY() { + return this.shape.scaleY(); + } + set scaleY(v: number) { + this.shape.scaleY(v); + this.redraw(); + } + get skewX() { + return this.shape.skewX(); + } + set skewX(v: number) { + this.shape.skewX(v); + this.redraw(); + } + get skewY() { + return this.shape.skewY(); + } + set skewY(v: number) { + this.shape.skewY(v); + this.redraw(); + } + get rotation() { + return this.shape.rotation(); + } + set rotation(v: number) { + this.shape.rotation(v); + this.redraw(); + } + get offsetX() { + return this.shape.offsetX(); + } + set offsetX(v: number) { + this.shape.offsetX(v); + this.redraw(); + } + get offsetY() { + return this.shape.offsetY(); + } + set offsetY(v: number) { + this.shape.offsetY(v); + this.redraw(); + } + get fillColor() { + return this.shape.fill().toString(); + } + set fillColor(v: string) { + this.shape.fill(v); + this.redraw(); + } + get fillPatternImage(): NfFile | undefined { + return this._fillPatternImage; + } + set fillPatternImage(v: NfFile) { + this.shape.fillPatternImage(v ? Object.assign(new Image(), { src: v.path }) : undefined); + this.redraw(); + } + get fillPatternX() { + return this.shape.fillPatternX(); + } + set fillPatternX(v: number) { + this.shape.fillPatternX(v); + this.redraw(); + } + get fillPatternY() { + return this.shape.fillPatternY(); + } + set fillPatternY(v: number) { + this.shape.fillPatternY(v); + this.redraw(); + } + get fillPatternOffsetX() { + return this.shape.fillPatternOffsetX(); + } + set fillPatternOffsetX(v: number) { + this.shape.fillPatternOffsetX(v); + this.redraw(); + } + get fillPatternOffsetY() { + return this.shape.fillPatternOffsetY(); + } + set fillPatternOffsetY(v: number) { + this.shape.fillPatternOffsetY(v); + this.redraw(); + } + get fillPatternScaleX() { + return this.shape.fillPatternScaleX(); + } + set fillPatternScaleX(v: number) { + this.shape.fillPatternScaleX(v); + this.redraw(); + } + get fillPatternScaleY() { + return this.shape.fillPatternScaleY(); + } + set fillPatternScaleY(v: number) { + this.shape.fillPatternScaleY(v); + this.redraw(); + } + get fillPatternRotation() { + return this.shape.fillPatternRotation(); + } + set fillPatternRotation(v: number) { + this.shape.fillPatternRotation(v); + this.redraw(); + } + get fillPatternRepeat() { + return this.shape.fillPatternRepeat(); + } + set fillPatternRepeat(v: string) { + this.shape.fillPatternRepeat(v); + this.redraw(); + } + + public addToLayer(layer: Layer): void { + if (this.shape.getParent() !== layer) layer.add(this.shape); + } + + private redraw() { + this.shape.getLayer()?.batchDraw(); + } +} +// * Required to generate code +export default DrawableCircle2D.name; + +// * Required for the editor to display the component and generate code +export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = { + name: "DrawableCircle2D", + description: "Graphically drawable circle 2D", + params: [ + { + name: "radius", + type: "number", + description: "Radius of the circle in pixels", + default: 10, + optional: false, + }, + { + name: "visible", + type: "boolean", + description: "Controls whether the node is rendered", + default: true, + optional: false, + }, + { + name: "id", + type: "string", + description: "Unique identifier for the node, used with stage.findOne('#id')", + optional: true, + }, + { + name: "opacity", + type: "number", + description: "Opacity from 0 (transparent) to 1 (fully opaque)", + default: 1, + optional: true, + }, + { + name: "scaleX", + type: "number", + description: "Horizontal scale factor", + default: 1, + optional: true, + }, + { + name: "scaleY", + type: "number", + description: "Vertical scale factor", + default: 1, + optional: true, + }, + { + name: "skewX", + type: "number", + default: 0, + description: "Horizontal skew in radians, shears along the X axis", + optional: true, + }, + { + name: "skewY", + type: "number", + default: 0, + description: "Vertical skew in radians, shears along the Y axis", + optional: true, + }, + { + name: "rotation", + type: "number", + description: + "Rotation angle in degrees applied around the offset point (positive values rotate clockwise)", + default: 0, + optional: true, + }, + { + name: "offsetX", + type: "number", + description: "Horizontal offset for the transform origin", + default: 0, + optional: true, + }, + { + name: "offsetY", + type: "number", + description: "Vertical offset for the transform origin", + default: 0, + optional: true, + }, + { + name: "fillColor", + type: "string", + description: + 'Solid fill color as a CSS color string (e.g. "red", "#ff0000", "rgba(255,0,0,0.5)"), or a pre-built CanvasGradient object', + default: "red", + example: "rgb(255,255,255)", + optional: true, + }, + { + name: "fillPatternImage", + type: "asset", + description: "Image element to use as a tiling pattern fill", + optional: true, + }, + { + name: "fillPatternX", + type: "number", + description: + "Horizontal offset of the pattern origin relative to the shape's top-left corner, in pixels", + optional: true, + }, + { + name: "fillPatternY", + type: "number", + description: + "Vertical offset of the pattern origin relative to the shape's top-left corner, in pixels", + optional: true, + }, + { + name: "fillPatternOffsetX", + type: "number", + description: "Horizontally shifts the pattern image within the tile before repeating", + optional: true, + }, + { + name: "fillPatternOffsetY", + type: "number", + description: "Vertically shifts the pattern image within the tile before repeating", + optional: true, + }, + { + name: "fillPatternScaleX", + type: "number", + description: "Horizontal scale factor for the pattern tile", + optional: true, + }, + { + name: "fillPatternScaleY", + type: "number", + description: "Vertical scale factor for the pattern tile", + optional: true, + }, + { + name: "fillPatternRotation", + type: "number", + description: "Rotation of the pattern tile in degrees", + optional: true, + }, + { + name: "fillPatternRepeat", + type: "string", + description: + 'Repeat mode for the pattern. One of "repeat" (default), "repeat-x", "repeat-y", or "no-repeat"', + optional: true, + }, + ], +}; \ No newline at end of file diff --git a/libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts b/libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts new file mode 100644 index 0000000..149cec5 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/src/components/position-2d.component.ts @@ -0,0 +1,35 @@ +import type { EditorComponentManifest } from "@nanoforge-dev/ecs"; + +export class Position2D { + name = this.constructor.name; + + constructor( + public x: number, + public y: number, + ) {} +} + +// * Required to generate code +export default Position2D.name; + +// * Required for the editor to display the component and generate code +export const EDITOR_COMPONENT_MANIFEST: EditorComponentManifest = { + name: "Position2D", + description: "Position of an entity in a 2 dimensional space in pixel", + params: [ + { + type: "number", + name: "x", + description: "Horizontal position in pixel", + example: 4.2, + default: 0, + }, + { + type: "number", + name: "y", + description: "Vertical position in pixel", + example: 67, + default: 0, + }, + ], +}; diff --git a/libs/schematics/src/libs/project/files/ts/src/main.ts b/libs/schematics/src/libs/project/files/ts/src/main.ts new file mode 100644 index 0000000..2402c78 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/src/main.ts @@ -0,0 +1,39 @@ +import { NanoforgeFactory, type <%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %> } from "nanoforge"; +import { EcsLibrary } from "@nanoforge-dev/ecs/<%= part %>";<% if (part === "client") { %> +import { Graphics2DLibrary } from "@nanoforge-dev/graphics-2d"; +import { InputLibrary } from "@nanoforge-dev/input";<% } %> +import { <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %> } from "@nanoforge-dev/network/<%= part %>"; +<% if (part === "client") { %> +import { DrawableCircle2D } from "./components/drawable-circle-2d.component";<% } %> +import { Position2D } from "./components/position-2d.component";<% if (part === "client") { %> +import { draw2D } from "./systems/draw-2d.system";<% if (hasServer) { %> +import { positionSync } from "./systems/position-sync.system";<% } %><% } else { %> +import { move2D } from "./systems/move-2d.system";<% } %> + +export const main = async (options: <%= part === "client" ? "ClientRunOptions" : "ServerRunOptions" %>): Promise => { + const app = NanoforgeFactory.<%= part === "client" ? "createClient" : "createServer" %>({ tickRate: 60 }); + + const ecs = new EcsLibrary(); + app.use(ecs);<% if (part === "client") { %> + app.use(new Graphics2DLibrary()); + app.use(new InputLibrary());<% } %> + app.use(new <%= part === "client" ? "NetworkClientLibrary" : "NetworkServerLibrary" %>()); + + await app.init(options); + + const registry = ecs.registry; +<% if (part === "client") { %> + const circle = registry.spawnEntity(); + registry.addComponent(circle, new Position2D(500, 500)); + registry.addComponent(circle, new DrawableCircle2D(75, true, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, "red")); + + registry.addSystem(draw2D);<% if (hasServer) { %> + registry.addSystem(positionSync);<% } %> +<% } else { %> + const entity = registry.spawnEntity(); + registry.addComponent(entity, new Position2D(500, 500)); + + registry.addSystem(move2D); +<% } %> + await app.run(); +}; diff --git a/libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts b/libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts new file mode 100644 index 0000000..eb075b6 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/src/systems/draw-2d.system.ts @@ -0,0 +1,25 @@ +import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs"; +import { type Context } from "nanoforge"; + +import { DrawableCircle2D } from "../components/drawable-circle-2d.component"; +import { Position2D } from "../components/position-2d.component"; + +export function draw2D(registry: Registry, ctx: Context) { + const graphic2d = ctx.graphics; + + const entities = registry.getZipper([Position2D, DrawableCircle2D]); + + entities.forEach(({ Position2D, DrawableCircle2D }) => { + DrawableCircle2D.addToLayer(graphic2d.baseLayer); + DrawableCircle2D.shape.setPosition(Position2D); + }); +} +// * Required to generate code +export default draw2D.name; + +// * Required for the editor to display the system and generate code +export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = { + name: "draw2D", + description: "Draw every drawable component", + dependencies: ["Position2D", "DrawableCircle2D"], +}; diff --git a/libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts b/libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts new file mode 100644 index 0000000..a980282 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/src/systems/move-2d.system.ts @@ -0,0 +1,26 @@ +import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs"; +import { type Context } from "nanoforge"; + +import { Position2D } from "../components/position-2d.component"; + +export function move2D(registry: Registry, ctx: Context) { + const network = ctx.network; + const entities = registry.getZipper([Position2D]); + + entities.forEach(({ Position2D }) => { + Position2D.x += 1; + + network.tcp.sendToEverybody( + new TextEncoder().encode(JSON.stringify({ x: Position2D.x, y: Position2D.y })), + ); + }); +} +// * Required to generate code +export default move2D.name; + +// * Required for the editor to display the system and generate code +export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = { + name: "move2D", + description: "Moves the entity and broadcasts its position to every connected client", + dependencies: ["Position2D"], +}; diff --git a/libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts b/libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts new file mode 100644 index 0000000..09fc85f --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/src/systems/position-sync.system.ts @@ -0,0 +1,27 @@ +import type { EditorSystemManifest, Registry } from "@nanoforge-dev/ecs"; +import { type Context } from "nanoforge"; + +import { Position2D } from "../components/position-2d.component"; + +export function positionSync(registry: Registry, ctx: Context) { + const network = ctx.network; + const packets = network.tcp + .getReceivedPackets() + .map((packet) => JSON.parse(new TextDecoder().decode(packet)) as { x: number; y: number }); + + const [entity] = registry.getZipper([Position2D]); + + packets.forEach((packet) => { + entity.Position2D.x = packet.x; + entity.Position2D.y = packet.y; + }); +} +// * Required to generate code +export default positionSync.name; + +// * Required for the editor to display the system and generate code +export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = { + name: "positionSync", + description: "Overwrites the local entity's position with the latest value received from the server", + dependencies: ["Position2D"], +}; diff --git a/libs/schematics/src/libs/project/files/ts/tsconfig.json b/libs/schematics/src/libs/project/files/ts/tsconfig.json new file mode 100644 index 0000000..70b5f64 --- /dev/null +++ b/libs/schematics/src/libs/project/files/ts/tsconfig.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json",<% if (workspace) { %> + "extends": "../../tsconfig.json",<% } else { %> + // Mapped from https://www.typescriptlang.org/tsconfig + "compilerOptions": { + // Type Checking + "allowUnreachableCode": <%= !strict %>, + "allowUnusedLabels": <%= !strict %>, + "exactOptionalPropertyTypes": <%= strict %>, + "noFallthroughCasesInSwitch": <%= strict %>, + "noImplicitOverride": <%= strict %>, + "noImplicitReturns": <%= strict %>, + "noPropertyAccessFromIndexSignature": <%= !strict %>, + "noUncheckedIndexedAccess": <%= strict %>, + "noUnusedLocals": <%= strict %>, + "noUnusedParameters": <%= strict %>, + "strict": <%= strict %>, + + // Modules + "allowArbitraryExtensions": false, + "allowImportingTsExtensions": false, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + + // Interop Constraints + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + + // Language and Environment + "experimentalDecorators": true, + "lib": ["ESNext", "esnext.disposable", "DOM"], + "target": "ESNext", + "useDefineForClassFields": true, + },<% } %> + "include": ["src"], + "exclude": ["node_modules", ".nanoforge", "dist"] +} diff --git a/libs/schematics/src/libs/project/project.factory.ts b/libs/schematics/src/libs/project/project.factory.ts new file mode 100644 index 0000000..dff4730 --- /dev/null +++ b/libs/schematics/src/libs/project/project.factory.ts @@ -0,0 +1,133 @@ +import { type Path, join, normalize, strings } from "@angular-devkit/core"; +import { + type Rule, + type Source, + apply, + filter, + mergeWith, + move, + renameTemplateFiles, + template, + url, +} from "@angular-devkit/schematics"; + +import { toKebabCase } from "@utils/formatting"; +import { fetchTrustedVersion } from "@utils/registry"; + +import { + DEFAULT_APP_NAME, + DEFAULT_LANGUAGE, + DEFAULT_NANOFORGE_DEPENDENCY_VERSION, + DEFAULT_PACKAGE_MANAGER, +} from "~/defaults"; + +import { type ProjectOptions } from "./project.options"; +import { type ProjectSchema } from "./project.schema"; + +interface DepVersions { + ecs: string; + graphics2d: string; + input: string; + network: string; + nanoforge: string; +} + +const resolveDepVersions = async (part: ProjectSchema["part"]): Promise => { + const isClient = part === "client"; + + const [nanoforge, ecs, network, graphics2d, input] = await Promise.all([ + fetchTrustedVersion("nanoforge", DEFAULT_NANOFORGE_DEPENDENCY_VERSION), + fetchTrustedVersion("@nanoforge-dev/ecs", DEFAULT_NANOFORGE_DEPENDENCY_VERSION), + fetchTrustedVersion("@nanoforge-dev/network", DEFAULT_NANOFORGE_DEPENDENCY_VERSION), + isClient + ? fetchTrustedVersion("@nanoforge-dev/graphics-2d", DEFAULT_NANOFORGE_DEPENDENCY_VERSION) + : DEFAULT_NANOFORGE_DEPENDENCY_VERSION, + isClient + ? fetchTrustedVersion("@nanoforge-dev/input", DEFAULT_NANOFORGE_DEPENDENCY_VERSION) + : DEFAULT_NANOFORGE_DEPENDENCY_VERSION, + ]); + + return { + ecs, + graphics2d, + input, + network, + nanoforge, + }; +}; + +const transform = async (schema: ProjectSchema): Promise => { + const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER; + const workspace = schema.workspace ?? true; + const depVersions = await resolveDepVersions(schema.part); + + const name = schema.name ?? schema.part; + const packageName = schema.workspaceName + ? `${toKebabCase(schema.workspaceName)}-${toKebabCase(name)}` + : toKebabCase(schema.name ?? DEFAULT_APP_NAME); + + return { + part: schema.part, + appClass: schema.part === "client" ? "NanoforgeClient" : "NanoforgeServer", + packageName, + language: schema.language ?? DEFAULT_LANGUAGE, + strict: schema.strict ?? true, + packageManager, + workspace, + ecsVersion: depVersions.ecs, + graphics2dVersion: depVersions.graphics2d, + inputVersion: depVersions.input, + networkVersion: depVersions.network, + nanoforgeVersion: depVersions.nanoforge, + hasServer: schema.hasServer, + docker: schema.docker ?? false, + editor: schema.editor ?? false, + allowBuilds: workspace ? [] : ["bun"], + }; +}; + +const generate = (options: ProjectOptions, path: string): Source => { + return apply(url(join("./files" as Path, options.language)), [ + template({ + dot: ".", + ...strings, + ...options, + }), + filter( + (path) => + (options.docker && !options.workspace) || + !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? ""), + ), + filter( + (path) => + (options.packageManager === "pnpm" && options.allowBuilds.length > 0) || + !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml"), + ), + filter((path) => { + const filename = path.split("/").at(-1) ?? ""; + if ( + filename.startsWith("drawable-circle-2d.component") || + filename.startsWith("draw-2d.system") + ) { + return options.part === "client"; + } + if (filename.startsWith("move-2d.system")) { + return options.part === "server"; + } + if (filename.startsWith("position-sync.system")) { + return options.part === "client" && options.hasServer; + } + return true; + }), + renameTemplateFiles(), + move(normalize(path)), + ]); +}; + +export const main = (schema: ProjectSchema): Rule => { + return async () => { + const options = await transform(schema); + + return mergeWith(generate(options, schema.directory)); + }; +}; diff --git a/libs/schematics/src/libs/project/project.options.d.ts b/libs/schematics/src/libs/project/project.options.d.ts new file mode 100644 index 0000000..ad4a4ab --- /dev/null +++ b/libs/schematics/src/libs/project/project.options.d.ts @@ -0,0 +1,82 @@ +export interface ProjectOptions { + /** + * The type of project to generate + */ + part: "client" | "server"; + + /** + * Fully-qualified Nanoforge app class name for the generated part, + * used in JSDoc type annotations for JavaScript projects + */ + appClass: string; + + /** + * The derived package.json name (from workspaceName and part) + */ + packageName: string; + + /** + * NanoForge project language + */ + language: "js" | "ts"; + + /** + * With strict mode (TypeScript or JavaScript but only on compatible editor) + */ + strict: boolean; + + /** + * The used package manager + */ + packageManager: "npm" | "yarn" | "pnpm" | "bun"; + + /** + * Whether this project is generated inside an existing NanoForge workspace + */ + workspace: boolean; + + /** + * Resolved version/protocol for the @nanoforge-dev/ecs dependency (aliased + * to @nanoforge-dev/ecs-lib for registry lookups; see project.factory.ts) + */ + ecsVersion: string; + + /** Resolved version/protocol for @nanoforge-dev/graphics-2d (client only) */ + graphics2dVersion: string; + + /** Resolved version/protocol for @nanoforge-dev/input (client only) */ + inputVersion: string; + + /** + * Resolved version/protocol for the @nanoforge-dev/network dependency + * (aliased to @nanoforge-dev/network-client or -server for registry + * lookups depending on `part`; see project.factory.ts) + */ + networkVersion: string; + + /** Resolved version/protocol for the nanoforge engine package */ + nanoforgeVersion: string; + + /** + * Only meaningful when `part` is "client": whether the client syncs its + * entity position from a companion server instead of owning it locally. + */ + hasServer: boolean; + + /** + * Generate a Dockerfile for the project. Ignored when `workspace` is true - a project + * inside a workspace is built and run by the workspace's own Dockerfile instead. + */ + docker: boolean; + + /** + * Add editor dependencies + */ + editor: boolean; + + /** + * Package names allowed to run install/build scripts. Only rendered when + * `workspace` is false — see {@link ProjectSchema.allowBuilds}. + */ + allowBuilds: string[]; +} diff --git a/libs/schematics/src/libs/project/project.schema.d.ts b/libs/schematics/src/libs/project/project.schema.d.ts new file mode 100644 index 0000000..5f00a9c --- /dev/null +++ b/libs/schematics/src/libs/project/project.schema.d.ts @@ -0,0 +1,67 @@ +export interface ProjectSchema { + /** + * The type of project to generate + */ + part: "client" | "server"; + + /** + * Name of the parent workspace, used to derive the project package name. + * Optional - when omitted, `name` is used as the whole package name + * instead of being appended to the workspace name. + */ + workspaceName?: string | null; + + /** + * Name used to derive the project package name. When `workspaceName` is + * set, the package name is `${workspaceName}-${name}` (replacing the + * previous `${workspaceName}-${part}` suffix). When `workspaceName` is + * absent, `name` becomes the whole package name. Defaults to `part`. + */ + name?: string | null; + + /** + * Full destination path for this project (e.g. "apps/client", + * "packages/player-client"). The caller is responsible for including any + * part-specific segment - it is not appended automatically. + */ + directory: string; + + /** + * NanoForge project language + */ + language: "js" | "ts"; + + /** + * With strict mode + */ + strict: boolean; + + /** + * The used package manager + */ + packageManager: "npm" | "yarn" | "pnpm" | "bun"; + + /** + * Whether this project is generated inside an existing NanoForge workspace + */ + workspace: boolean; + + /** + * Generate a Dockerfile for the project. Ignored when `workspace` is true - a project + * inside a workspace is built and run by the workspace's own Dockerfile instead. + */ + docker: boolean; + + /** + * Only meaningful when `part` is "client": whether the client syncs its + * entity position from a companion server instead of owning it locally. + * A server project always simulates and broadcasts its own position, + * regardless of this flag. + */ + hasServer: boolean; + + /** + * Add editor dependencies + */ + editor: boolean; +} diff --git a/libs/schematics/src/libs/project/schema.json b/libs/schematics/src/libs/project/schema.json new file mode 100644 index 0000000..cc933df --- /dev/null +++ b/libs/schematics/src/libs/project/schema.json @@ -0,0 +1,83 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsNanoForgeProject", + "title": "NanoForge Project Options Schema", + "type": "object", + "properties": { + "part": { + "type": "string", + "enum": ["client", "server"], + "description": "The type of project to generate" + }, + "workspaceName": { + "type": ["string", "null"], + "description": "Name of the parent workspace, used to derive the project package name. Optional - when omitted, `name` is used as the whole package name instead of being appended to the workspace name.", + "default": null + }, + "name": { + "type": ["string", "null"], + "description": "Name used to derive the project package name. When `workspaceName` is set, the package name is `${workspaceName}-${name}` (replacing the previous `${workspaceName}-${part}` suffix). When `workspaceName` is absent, `name` becomes the whole package name. Defaults to `part`.", + "default": null + }, + "directory": { + "type": "string", + "description": "Full destination path for this project (e.g. \"apps/client\", \"packages/player-client\"). The caller is responsible for including any part-specific segment - it is not appended automatically.", + "default": "apps" + }, + "language": { + "type": "string", + "enum": ["ts", "js"], + "description": "NanoForge project language", + "default": "ts" + }, + "strict": { + "type": "boolean", + "description": "With strict mode", + "default": true + }, + "packageManager": { + "type": "string", + "enum": ["npm", "yarn", "pnpm", "bun"], + "description": "The used package manager", + "default": "npm" + }, + "workspace": { + "type": "boolean", + "description": "Whether this project is generated inside an existing NanoForge workspace", + "default": true + }, + "docker": { + "type": "boolean", + "description": "Generate a Dockerfile for the project. Ignored when `workspace` is true - a project inside a workspace is built and run by the workspace's own Dockerfile instead.", + "default": false + }, + "initFunctions": { + "type": "boolean", + "description": "Add init functions to the project", + "default": false + }, + "hasServer": { + "type": "boolean", + "description": "Only meaningful when part is \"client\": whether the client syncs its entity position from a companion server instead of owning it locally. A server project always simulates and broadcasts its own position, regardless of this flag.", + "default": false + }, + "editor": { + "type": "boolean", + "description": "Add editor dependencies", + "default": false + }, + "libs": { + "type": "array", + "items": { "type": "string" }, + "description": "Relative paths to shared libs this project depends on", + "default": [] + }, + "allowBuilds": { + "type": "array", + "items": { "type": "string" }, + "description": "Package names allowed to run install/build scripts. Only applies to a standalone project (workspace: false); ignored otherwise since the workspace root already handles it.", + "default": [] + } + }, + "required": ["part"] +} diff --git a/libs/schematics/src/libs/system/files/js/__fileName__.system.js b/libs/schematics/src/libs/system/files/js/__fileName__.system.js new file mode 100644 index 0000000..64240a7 --- /dev/null +++ b/libs/schematics/src/libs/system/files/js/__fileName__.system.js @@ -0,0 +1,44 @@ +/** + * @typedef {import("@nanoforge-dev/common").Context} Context + * @typedef {import("@nanoforge-dev/ecs-<%= part %>").EditorSystemManifest} EditorSystemManifest + * @typedef {import("@nanoforge-dev/ecs-<%= part %>").Registry} Registry + */ + +import { ExampleComponent } from "../components/example.component"; + +/** + * <%= functionName %> system + * This system end the game when paramB reaches 0 for any entity with ExampleComponent + * @param {Registry} registry - ECS registry instance + * @param {Context} ctx - Nanoforge <%= part %> instance + */ +export const <%= functionName %>System = (registry, ctx) => { + const entities = registry.getZipper([ExampleComponent]); + + entities.forEach((entity) => { + if (entity.ExampleComponent.paramA === "end") { + ctx.app.setIsRunning(false); + return; + } + + if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end"; + + if (entity.ExampleComponent.paramB >= 0) + entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1; + }); +}; + +// * Required to generate code +export default <%= functionName %>System.name; + +/** + * Editor component manifest + * * Required for the editor to display the system and generate code + * @type {EditorSystemManifest} + */ +export const EDITOR_SYSTEM_MANIFEST = { + name: "<%= functionName %>", + description: + "This system end the game when paramB reaches 0 for any entity with ExampleComponent", + dependencies: ["ExampleComponent"], +}; diff --git a/libs/schematics/src/libs/system/files/ts/__fileName__.system.ts b/libs/schematics/src/libs/system/files/ts/__fileName__.system.ts new file mode 100644 index 0000000..d1f9ce6 --- /dev/null +++ b/libs/schematics/src/libs/system/files/ts/__fileName__.system.ts @@ -0,0 +1,31 @@ +import { type Context } from "@nanoforge-dev/common"; +import { type EditorSystemManifest, type Registry } from "@nanoforge-dev/ecs-<%= part %>"; + +import { ExampleComponent } from "../components/example.component"; + +export const <%= functionName %>System = (registry: Registry, ctx: Context) => { + const entities = registry.getZipper([ExampleComponent]); + + entities.forEach((entity) => { + if (entity.ExampleComponent.paramA === "end") { + ctx.app.setIsRunning(false); + return; + } + + if (entity.ExampleComponent.paramB === 0) entity.ExampleComponent.paramA = "end"; + + if (entity.ExampleComponent.paramB >= 0) + entity.ExampleComponent.paramB = entity.ExampleComponent.paramB - 1; + }); +}; + +// * Required to generate code +export default <%= functionName %>System.name; + +// * Required for the editor to display the system and generate code +export const EDITOR_SYSTEM_MANIFEST: EditorSystemManifest = { + name: "<%= functionName %>", + description: + "This system end the game when paramB reaches 0 for any entity with ExampleComponent", + dependencies: ["ExampleComponent"], +}; diff --git a/libs/schematics/src/libs/system/schema.json b/libs/schematics/src/libs/system/schema.json new file mode 100644 index 0000000..0578ac2 --- /dev/null +++ b/libs/schematics/src/libs/system/schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsNanoForgeSystem", + "title": "NanoForge System Options Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "System name", + "default": "example" + }, + "directory": { + "type": "string", + "description": "System destination directory", + "default": "." + }, + "part": { + "type": "string", + "enum": ["client", "server"], + "description": "The part of the application to generate" + }, + "language": { + "type": "string", + "enum": ["ts", "js"], + "description": "NanoForge application language", + "default": "ts" + } + }, + "required": ["part"] +} diff --git a/libs/schematics/src/libs/system/system.factory.ts b/libs/schematics/src/libs/system/system.factory.ts new file mode 100644 index 0000000..93fabde --- /dev/null +++ b/libs/schematics/src/libs/system/system.factory.ts @@ -0,0 +1,41 @@ +import { type Path, join, normalize, strings } from "@angular-devkit/core"; +import { + type Rule, + type Source, + apply, + mergeWith, + move, + template, + url, +} from "@angular-devkit/schematics"; + +import { toCamelCase, toKebabCase } from "@utils/formatting"; + +import { type SystemOptions } from "./system.options"; +import { type SystemSchema } from "./system.schema"; + +const transform = (schema: SystemSchema): SystemOptions => { + return { + part: schema.part, + functionName: toCamelCase(schema.name), + fileName: toKebabCase(schema.name), + }; +}; + +const generate = (options: SystemOptions, path: string, language: "ts" | "js"): Source => { + const rules = [ + template({ + ...strings, + ...options, + }), + move(normalize(path)), + ]; + + return apply(url(join("./files" as Path, language)), rules); +}; + +export const main = (schema: SystemSchema): Rule => { + const options = transform(schema); + + return mergeWith(generate(options, schema.directory, schema.language)); +}; diff --git a/libs/schematics/src/libs/system/system.options.d.ts b/libs/schematics/src/libs/system/system.options.d.ts new file mode 100644 index 0000000..c16d2e6 --- /dev/null +++ b/libs/schematics/src/libs/system/system.options.d.ts @@ -0,0 +1,16 @@ +export interface SystemOptions { + /** + * The part of the application to generate + */ + part: "client" | "server"; + + /** + * System function name + */ + functionName: string; + + /** + * System filename + */ + fileName: string; +} diff --git a/libs/schematics/src/libs/system/system.schema.d.ts b/libs/schematics/src/libs/system/system.schema.d.ts new file mode 100644 index 0000000..19c7bfe --- /dev/null +++ b/libs/schematics/src/libs/system/system.schema.d.ts @@ -0,0 +1,21 @@ +export interface SystemSchema { + /** + * System destination directory + */ + name: string; + + /** + * System destination directory + */ + directory: string; + + /** + * The part of the application to generate + */ + part: "client" | "server"; + + /** + * NanoForge Application language + */ + language: "js" | "ts"; +} diff --git a/libs/schematics/src/libs/workspace/files/js/.dockerignore b/libs/schematics/src/libs/workspace/files/js/.dockerignore new file mode 100644 index 0000000..1c13727 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/.dockerignore @@ -0,0 +1,5 @@ +**/node_modules/ +**/dist/ +**/.nanoforge/ +**/.env +.git/ diff --git a/libs/schematics/src/libs/workspace/files/js/Dockerfile b/libs/schematics/src/libs/workspace/files/js/Dockerfile new file mode 100644 index 0000000..c28346e --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/Dockerfile @@ -0,0 +1,36 @@ +<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base +<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base +RUN pnpm runtime set node 26 -g +<% } else { %>FROM node:26-slim AS base +<% } %><% if (packageManager === "yarn") { %> +RUN npm install -g yarn +<% } %> +WORKDIR /app + +FROM base AS deps +<% if (packageManager === "pnpm") { %> +COPY pnpm-lock.yaml ./ +RUN pnpm fetch + +FROM deps AS build +COPY . . +RUN pnpm install --frozen-lockfile --offline +<% } else if (packageManager === "npm") { %> +FROM deps AS build +COPY . . +RUN npm ci +<% } else { %> +FROM deps AS build +COPY . . +RUN <%= packageManager %> install --frozen-lockfile +<% } %> +RUN <%= packageManager %> run build + +<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %> +WORKDIR /app + +COPY --from=build /app . +<% if (packageManager !== "pnpm") { %> +USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %> +<% } %>EXPOSE 3000 +CMD ["<%= packageManager %>", "run", "start"] diff --git a/libs/schematics/src/libs/workspace/files/js/README.md b/libs/schematics/src/libs/workspace/files/js/README.md new file mode 100644 index 0000000..cf7bc11 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/README.md @@ -0,0 +1,51 @@ +# <%= name %> + +A [NanoForge](https://nanoforge.eu) game workspace. + +## Prerequisites + +- Node.js and <%= packageManager %> +- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli` + +## Install + +```bash +<%= packageManager %> install +``` + +## Structure + +This is the root of your NanoForge monorepo. `nanoforge.config.js` lists the app packages, and each +client or server app lives under `apps/` (generated separately with the `project` schematic). + +## Development + +```bash +nf dev -r +``` + +`-r` runs the command across every app in the workspace. `nf dev` serves the game and watches your +files, so changes are picked up as you work. Target a single app instead with `-d apps/`. + +## Build & start + +```bash +nf build -r +nf start -r +``` + +`nf build` compiles every app for production, and `nf start` serves the build. + +## CLI reference + +| Command | Description | +| ---------- | ------------------------------------- | +| `nf dev` | Develop with hot reload | +| `nf build` | Build for production | +| `nf start` | Run a built app | +| `-r` | Apply the command to every app | + +## More info + +- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides +- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge diff --git a/libs/schematics/src/libs/workspace/files/js/__dot__env b/libs/schematics/src/libs/workspace/files/js/__dot__env new file mode 100644 index 0000000..75b4595 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/__dot__env @@ -0,0 +1,6 @@ +NANOFORGE_CLIENT_SERVER_TCP_PORT=4444 +NANOFORGE_CLIENT_SERVER_UDP_PORT=4445 +NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1 + +NANOFORGE_SERVER_LISTENING_TCP_PORT=4444 +NANOFORGE_SERVER_LISTENING_UDP_PORT=4445 diff --git a/libs/schematics/src/libs/workspace/files/js/__dot__gitignore b/libs/schematics/src/libs/workspace/files/js/__dot__gitignore new file mode 100644 index 0000000..6857e7d --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/__dot__gitignore @@ -0,0 +1,137 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# File-based project format +*.iws + +# Editor-based Rest Client +.idea/httpRequests + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Turborepo +.turbo/ + +### NanoForge template +# Build/compilation output +.nanoforge/ +dist/ diff --git a/libs/schematics/src/libs/workspace/files/js/jsconfig.json b/libs/schematics/src/libs/workspace/files/js/jsconfig.json new file mode 100644 index 0000000..4b7a35e --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/jsconfig.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/jsconfig.json", + "compilerOptions": { + "allowUnreachableCode": <%= !strict %>, + "allowUnusedLabels": <%= !strict %>, + "exactOptionalPropertyTypes": <%= strict %>, + "noFallthroughCasesInSwitch": <%= strict %>, + "noImplicitOverride": <%= strict %>, + "noImplicitReturns": <%= strict %>, + "noPropertyAccessFromIndexSignature": <%= !strict %>, + "noUncheckedIndexedAccess": <%= strict %>, + "noUnusedLocals": <%= strict %>, + "noUnusedParameters": <%= strict %>, + + "allowArbitraryExtensions": false, + "allowImportingTsExtensions": false, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + + "experimentalDecorators": true, + "lib": ["ESNext", "esnext.disposable", "DOM"], + "target": "ESNext", + "useDefineForClassFields": true + }, + "exclude": ["node_modules", ".nanoforge", "dist"] +} diff --git a/libs/schematics/src/libs/workspace/files/js/nanoforge.config.js b/libs/schematics/src/libs/workspace/files/js/nanoforge.config.js new file mode 100644 index 0000000..1bbf8d4 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/nanoforge.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from "nanoforge/config"; + +export default defineConfig({ + type: "workspace", + packages: ["apps/*"], +}); diff --git a/libs/schematics/src/libs/workspace/files/js/package.json.template b/libs/schematics/src/libs/workspace/files/js/package.json.template new file mode 100644 index 0000000..cfd2c9a --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/package.json.template @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "<%= name %>", + "scripts": { + "dev": "nf dev -r", + "build": "nf build -r", + "start": "nf start -r" + }, + "devDependencies": { + "@nanoforge-dev/cli": "<%= cliVersion %>", + "nanoforge": "<%= engineVersion %>" + },<% if (packageManager !== "pnpm") { %> + "workspaces": ["apps/*"],<% } %><% if (packageManager === "npm" && allowBuilds.length > 0) { %> + "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %> + "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %> + },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %> + "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %> + "private": true +} diff --git a/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template b/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template new file mode 100644 index 0000000..34f3b9c --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/js/pnpm-workspace.yaml.template @@ -0,0 +1,6 @@ +packages: + - "apps/*" +<% if (allowBuilds.length > 0) { %> +allowBuilds:<% allowBuilds.forEach(function (pkg) { %> + <%= pkg %>: true<% }); %> +<% } %> \ No newline at end of file diff --git a/libs/schematics/src/libs/workspace/files/ts/.dockerignore b/libs/schematics/src/libs/workspace/files/ts/.dockerignore new file mode 100644 index 0000000..1c13727 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/.dockerignore @@ -0,0 +1,5 @@ +**/node_modules/ +**/dist/ +**/.nanoforge/ +**/.env +.git/ diff --git a/libs/schematics/src/libs/workspace/files/ts/Dockerfile b/libs/schematics/src/libs/workspace/files/ts/Dockerfile new file mode 100644 index 0000000..c28346e --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/Dockerfile @@ -0,0 +1,36 @@ +<% if (packageManager === "bun") { %>FROM oven/bun:1.3 AS base +<% } else if (packageManager === "pnpm") { %>FROM ghcr.io/pnpm/pnpm:12 AS base +RUN pnpm runtime set node 26 -g +<% } else { %>FROM node:26-slim AS base +<% } %><% if (packageManager === "yarn") { %> +RUN npm install -g yarn +<% } %> +WORKDIR /app + +FROM base AS deps +<% if (packageManager === "pnpm") { %> +COPY pnpm-lock.yaml ./ +RUN pnpm fetch + +FROM deps AS build +COPY . . +RUN pnpm install --frozen-lockfile --offline +<% } else if (packageManager === "npm") { %> +FROM deps AS build +COPY . . +RUN npm ci +<% } else { %> +FROM deps AS build +COPY . . +RUN <%= packageManager %> install --frozen-lockfile +<% } %> +RUN <%= packageManager %> run build + +<% if (packageManager === "bun") { %>FROM oven/bun:1.3-slim<% } else { %>FROM base<% } %> +WORKDIR /app + +COPY --from=build /app . +<% if (packageManager !== "pnpm") { %> +USER <% if (packageManager === "bun") { %>bun<% } else { %>node<% } %> +<% } %>EXPOSE 3000 +CMD ["<%= packageManager %>", "run", "start"] diff --git a/libs/schematics/src/libs/workspace/files/ts/README.md b/libs/schematics/src/libs/workspace/files/ts/README.md new file mode 100644 index 0000000..7a6c2c6 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/README.md @@ -0,0 +1,51 @@ +# <%= name %> + +A [NanoForge](https://nanoforge.eu) game workspace. + +## Prerequisites + +- Node.js and <%= packageManager %> +- The [NanoForge CLI](https://www.npmjs.com/package/@nanoforge-dev/cli): `npm install -g @nanoforge-dev/cli` + +## Install + +```bash +<%= packageManager %> install +``` + +## Structure + +This is the root of your NanoForge monorepo. `nanoforge.config.ts` lists the app packages, and each +client or server app lives under `apps/` (generated separately with the `project` schematic). + +## Development + +```bash +nf dev -r +``` + +`-r` runs the command across every app in the workspace. `nf dev` serves the game and watches your +files, so changes are picked up as you work. Target a single app instead with `-d apps/`. + +## Build & start + +```bash +nf build -r +nf start -r +``` + +`nf build` compiles every app for production, and `nf start` serves the build. + +## CLI reference + +| Command | Description | +| ---------- | ------------------------------------- | +| `nf dev` | Develop with hot reload | +| `nf build` | Build for production | +| `nf start` | Run a built app | +| `-r` | Apply the command to every app | + +## More info + +- [Documentation](https://docs.nanoforge.eu) - CLI commands, configuration, and guides +- [nanoforge.eu](https://nanoforge.eu) - learn more about NanoForge diff --git a/libs/schematics/src/libs/workspace/files/ts/__dot__env b/libs/schematics/src/libs/workspace/files/ts/__dot__env new file mode 100644 index 0000000..75b4595 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/__dot__env @@ -0,0 +1,6 @@ +NANOFORGE_CLIENT_SERVER_TCP_PORT=4444 +NANOFORGE_CLIENT_SERVER_UDP_PORT=4445 +NANOFORGE_CLIENT_SERVER_ADDRESS=127.0.0.1 + +NANOFORGE_SERVER_LISTENING_TCP_PORT=4444 +NANOFORGE_SERVER_LISTENING_UDP_PORT=4445 diff --git a/libs/schematics/src/libs/workspace/files/ts/__dot__gitignore b/libs/schematics/src/libs/workspace/files/ts/__dot__gitignore new file mode 100644 index 0000000..fc0f862 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/__dot__gitignore @@ -0,0 +1,134 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### JetBrains template + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# File-based project format +*.iws + +# Editor-based Rest Client +.idea/httpRequests + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### NanoForge template +# Build/compilation output +.nanoforge/ +dist/ diff --git a/libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts b/libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts new file mode 100644 index 0000000..1bbf8d4 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/nanoforge.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "nanoforge/config"; + +export default defineConfig({ + type: "workspace", + packages: ["apps/*"], +}); diff --git a/libs/schematics/src/libs/workspace/files/ts/package.json.template b/libs/schematics/src/libs/workspace/files/ts/package.json.template new file mode 100644 index 0000000..29b7e4b --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/package.json.template @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "<%= name %>", + "scripts": { + "dev": "nf dev -r", + "build": "nf build -r", + "start": "nf start -r" + }, + "devDependencies": { + "@nanoforge-dev/cli": "<%= cliVersion %>", + "nanoforge": "<%= engineVersion %>", + "typescript": "<%= typescriptVersion %>" + },<% if (packageManager !== "pnpm") { %> + "workspaces": ["apps/*"],<% } %><% if (packageManager === "npm" && allowBuilds.length > 0) { %> + "allowScripts": {<% allowBuilds.forEach(function (pkg, i) { %> + "<%= pkg %>": true<% if (i < allowBuilds.length - 1) { %>,<% } %><% }); %> + },<% } %><% if (packageManager === "bun" && allowBuilds.length > 0) { %> + "trustedDependencies": [<% allowBuilds.forEach(function (pkg, i) { %>"<%= pkg %>"<% if (i < allowBuilds.length - 1) { %>, <% } %><% }); %>],<% } %> + "private": true +} diff --git a/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template b/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template new file mode 100644 index 0000000..34f3b9c --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/pnpm-workspace.yaml.template @@ -0,0 +1,6 @@ +packages: + - "apps/*" +<% if (allowBuilds.length > 0) { %> +allowBuilds:<% allowBuilds.forEach(function (pkg) { %> + <%= pkg %>: true<% }); %> +<% } %> \ No newline at end of file diff --git a/libs/schematics/src/libs/workspace/files/ts/tsconfig.json b/libs/schematics/src/libs/workspace/files/ts/tsconfig.json new file mode 100644 index 0000000..5156549 --- /dev/null +++ b/libs/schematics/src/libs/workspace/files/ts/tsconfig.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + // Mapped from https://www.typescriptlang.org/tsconfig + "compilerOptions": { + // Type Checking + "allowUnreachableCode": <%= !strict %>, + "allowUnusedLabels": <%= !strict %>, + "exactOptionalPropertyTypes": <%= strict %>, + "noFallthroughCasesInSwitch": <%= strict %>, + "noImplicitOverride": <%= strict %>, + "noImplicitReturns": <%= strict %>, + "noPropertyAccessFromIndexSignature": <%= !strict %>, + "noUncheckedIndexedAccess": <%= strict %>, + "noUnusedLocals": <%= strict %>, + "noUnusedParameters": <%= strict %>, + "strict": <%= strict %>, + + // Modules + "allowArbitraryExtensions": false, + "allowImportingTsExtensions": false, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + + // Interop Constraints + "allowSyntheticDefaultImports": true, + "esModuleInterop": false, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + + // Language and Environment + "experimentalDecorators": true, + "lib": ["ESNext", "esnext.disposable", "DOM"], + "target": "ESNext", + "useDefineForClassFields": true, + }, + "exclude": ["node_modules", ".nanoforge", "dist"] +} diff --git a/libs/schematics/src/libs/workspace/schema.json b/libs/schematics/src/libs/workspace/schema.json new file mode 100644 index 0000000..9d843e6 --- /dev/null +++ b/libs/schematics/src/libs/workspace/schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "SchematicsNanoForgeWorkspace", + "title": "NanoForge Workspace Options Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the workspace", + "default": "nanoforge-app" + }, + "directory": { + "type": "string", + "description": "NanoForge workspace destination directory (defaults to the workspace name when omitted)" + }, + "language": { + "type": "string", + "enum": ["ts", "js"], + "description": "NanoForge workspace language", + "default": "ts" + }, + "strict": { + "type": "boolean", + "description": "With strict mode (TypeScript or JavaScript but only on compatible editor)", + "default": true + }, + "packageManager": { + "type": "string", + "enum": ["npm", "yarn", "pnpm", "bun"], + "description": "NanoForge workspace package manager", + "default": "npm" + }, + "allowBuilds": { + "type": "array", + "items": { "type": "string" }, + "description": "Package names allowed to run install/build scripts, in addition to `bun` which is always allowed (@nanoforge-dev/cli depends on it and it has its own install script). Written to pnpm-workspace.yaml (pnpm), package.json's allowScripts (npm) or trustedDependencies (bun). Ignored for yarn.", + "default": [] + }, + "docker": { + "type": "boolean", + "description": "Generate a Dockerfile building every app in the workspace into one image", + "default": false + } + } +} diff --git a/libs/schematics/src/libs/workspace/workspace.factory.ts b/libs/schematics/src/libs/workspace/workspace.factory.ts new file mode 100644 index 0000000..61cb091 --- /dev/null +++ b/libs/schematics/src/libs/workspace/workspace.factory.ts @@ -0,0 +1,84 @@ +import { type Path, join, normalize, strings } from "@angular-devkit/core"; +import { + type Rule, + type Source, + apply, + filter, + mergeWith, + move, + renameTemplateFiles, + template, + url, +} from "@angular-devkit/schematics"; + +import { fetchTrustedVersions } from "@utils/registry"; + +import { + DEFAULT_CLI_DEPENDENCY_VERSION, + DEFAULT_ENGINE_VERSION, + DEFAULT_PACKAGE_MANAGER, + DEFAULT_TYPESCRIPT_VERSION, +} from "~/defaults"; + +import { type WorkspaceOptions } from "./workspace.options"; +import { type WorkspaceSchema } from "./workspace.schema"; + +/** + * bun ships as a postinstall-driven binary download (its own package.json + * declares `"postinstall": "node install.js"`), and @nanoforge-dev/cli + * depends on it unconditionally, so every generated workspace needs it + * approved to run its build script regardless of what the caller passes. + */ +const ALWAYS_ALLOWED_BUILDS = ["bun"]; + +const transform = async (schema: WorkspaceSchema): Promise => { + const packageManager = schema.packageManager ?? DEFAULT_PACKAGE_MANAGER; + + const versions = await fetchTrustedVersions({ + "@nanoforge-dev/cli": { fallback: DEFAULT_CLI_DEPENDENCY_VERSION }, + nanoforge: { fallback: DEFAULT_ENGINE_VERSION }, + typescript: { fallback: DEFAULT_TYPESCRIPT_VERSION, major: 6 }, + }); + + return { + name: schema.name, + engineVersion: versions.nanoforge, + cliVersion: versions["@nanoforge-dev/cli"], + typescriptVersion: versions.typescript, + language: schema.language, + strict: schema.strict, + packageManager, + allowBuilds: Array.from(new Set([...ALWAYS_ALLOWED_BUILDS, ...(schema.allowBuilds ?? [])])), + docker: schema.docker ?? false, + }; +}; + +const generate = (options: WorkspaceOptions, path: string): Source => { + return apply(url(join("./files" as Path, options.language)), [ + template({ + dot: ".", + ...strings, + ...options, + }), + filter( + (path) => + options.packageManager === "pnpm" || + !(path.split("/").at(-1) ?? "").startsWith("pnpm-workspace.yaml"), + ), + filter( + (path) => + options.docker || !["Dockerfile", ".dockerignore"].includes(path.split("/").at(-1) ?? ""), + ), + renameTemplateFiles(), + move(normalize(path)), + ]); +}; + +export const main = (schema: WorkspaceSchema): Rule => { + return async () => { + const options = await transform(schema); + const path = schema.directory ?? options.name; + + return mergeWith(generate(options, path)); + }; +}; diff --git a/libs/schematics/src/libs/workspace/workspace.options.d.ts b/libs/schematics/src/libs/workspace/workspace.options.d.ts new file mode 100644 index 0000000..bcc9408 --- /dev/null +++ b/libs/schematics/src/libs/workspace/workspace.options.d.ts @@ -0,0 +1,44 @@ +export interface WorkspaceOptions { + /** + * NanoForge workspace name + */ + name: string; + + /** + * Version of engine packages (nanoforge, @nanoforge-dev/cli), + * resolved to the newest registry release that respects MIN_RELEASE_AGE_HOURS. + */ + engineVersion: string; + + /** Resolved @nanoforge-dev/cli version/protocol (see {@link engineVersion}). */ + cliVersion: string; + + /** Resolved typescript version (TypeScript language only). */ + typescriptVersion: string; + + /** + * NanoForge workspace language + */ + language: "js" | "ts"; + + /** + * With strict mode (TypeScript or JavaScript but only on compatible editor) + */ + strict: boolean; + + /** + * The used package manager + */ + packageManager: "npm" | "yarn" | "pnpm" | "bun"; + + /** + * Package names allowed to run install/build scripts (always includes `bun`). + * See {@link WorkspaceSchema.allowBuilds}. + */ + allowBuilds: string[]; + + /** + * Generate a Dockerfile building every app in the workspace into one image + */ + docker: boolean; +} diff --git a/libs/schematics/src/libs/workspace/workspace.schema.d.ts b/libs/schematics/src/libs/workspace/workspace.schema.d.ts new file mode 100644 index 0000000..fd61e17 --- /dev/null +++ b/libs/schematics/src/libs/workspace/workspace.schema.d.ts @@ -0,0 +1,41 @@ +export interface WorkspaceSchema { + /** + * NanoForge workspace name + */ + name: string; + + /** + * NanoForge workspace destination directory + */ + directory: string; + + /** + * NanoForge workspace language + */ + language: "js" | "ts"; + + /** + * With strict mode (TypeScript or JavaScript but only on compatible editor) + */ + strict: boolean; + + /** + * The used package manager + */ + packageManager: "npm" | "yarn" | "pnpm" | "bun"; + + /** + * Package names allowed to run install/build scripts. Written to + * pnpm-workspace.yaml's `allowBuilds` map (pnpm), package.json's + * `allowScripts` map (npm) or `trustedDependencies` array (bun). Ignored + * for yarn. `bun` is always included in addition to this list since + * @nanoforge-dev/cli transitively depends on it and it has its own + * install script. + */ + allowBuilds: string[]; + + /** + * Generate a Dockerfile building every app in the workspace into one image + */ + docker: boolean; +} diff --git a/libs/schematics/src/utils/formatting.spec.ts b/libs/schematics/src/utils/formatting.spec.ts new file mode 100644 index 0000000..dfaf4b4 --- /dev/null +++ b/libs/schematics/src/utils/formatting.spec.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; + +import { toCamelCase, toKebabCase, toPascalCase } from "./formatting"; + +describe("toKebabCase", () => { + it("should convert camelCase to kebab-case", () => { + expect(toKebabCase("myVariableName")).toBe("my-variable-name"); + }); + + it("should convert PascalCase to kebab-case", () => { + expect(toKebabCase("MyVariableName")).toBe("my-variable-name"); + }); + + it("should convert spaces to hyphens", () => { + expect(toKebabCase("my variable name")).toBe("my-variable-name"); + }); + + it("should convert underscores to hyphens", () => { + expect(toKebabCase("my_variable_name")).toBe("my-variable-name"); + }); + + it("should return an already kebab-case string unchanged", () => { + expect(toKebabCase("my-variable-name")).toBe("my-variable-name"); + }); + + it("should handle a single word", () => { + expect(toKebabCase("hello")).toBe("hello"); + }); + + it("should handle an empty string", () => { + expect(toKebabCase("")).toBe(""); + }); +}); + +describe("toPascalCase", () => { + it("should convert camelCase to PascalCase", () => { + expect(toPascalCase("myVariableName")).toBe("MyVariableName"); + }); + + it("should convert kebab-case to PascalCase", () => { + expect(toPascalCase("my-variable-name")).toBe("MyVariableName"); + }); + + it("should convert snake_case to PascalCase", () => { + expect(toPascalCase("my_variable_name")).toBe("MyVariableName"); + }); + + it("should convert spaces to PascalCase", () => { + expect(toPascalCase("my variable name")).toBe("MyVariableName"); + }); + + it("should handle already PascalCase string", () => { + expect(toPascalCase("MyVariableName")).toBe("MyVariableName"); + }); + + it("should handle a single word", () => { + expect(toPascalCase("hello")).toBe("Hello"); + }); + + it("should handle an empty string", () => { + expect(toPascalCase("")).toBe(""); + }); + + it("should handle SCREAMING_SNAKE_CASE", () => { + expect(toPascalCase("MY_VARIABLE_NAME")).toBe("MyVariableName"); + }); +}); + +describe("toCamelCase", () => { + it("should convert kebab-case to camelCase", () => { + expect(toCamelCase("my-variable-name")).toBe("myVariableName"); + }); + + it("should convert snake_case to camelCase", () => { + expect(toCamelCase("my_variable_name")).toBe("myVariableName"); + }); + + it("should convert spaces to camelCase", () => { + expect(toCamelCase("my variable name")).toBe("myVariableName"); + }); + + it("should handle already camelCase string", () => { + expect(toCamelCase("myVariableName")).toBe("myVariableName"); + }); + + it("should convert PascalCase to camelCase", () => { + expect(toCamelCase("MyVariableName")).toBe("myVariableName"); + }); + + it("should handle a single word", () => { + expect(toCamelCase("hello")).toBe("hello"); + }); + + it("should handle an empty string", () => { + expect(toCamelCase("")).toBe(""); + }); + + it("should handle SCREAMING_SNAKE_CASE", () => { + expect(toCamelCase("MY_VARIABLE_NAME")).toBe("myVariableName"); + }); +}); diff --git a/libs/schematics/src/utils/formatting.ts b/libs/schematics/src/utils/formatting.ts new file mode 100644 index 0000000..d86154e --- /dev/null +++ b/libs/schematics/src/utils/formatting.ts @@ -0,0 +1,30 @@ +const toWords = (str: string): string[] => { + return str + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .replace(/[-_]+/g, " ") + .trim() + .split(/\s+/) + .filter(Boolean); +}; + +export const toKebabCase = (str: string): string => { + return toWords(str) + .map((word) => word.toLowerCase()) + .join("-"); +}; + +export const toPascalCase = (str: string): string => { + return toWords(str) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(""); +}; + +export const toCamelCase = (str: string): string => { + const words = toWords(str); + return words + .map((word, i) => + i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join(""); +}; diff --git a/libs/schematics/src/utils/name.spec.ts b/libs/schematics/src/utils/name.spec.ts new file mode 100644 index 0000000..350acbf --- /dev/null +++ b/libs/schematics/src/utils/name.spec.ts @@ -0,0 +1,23 @@ +import { basename } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { resolvePackageName } from "./name"; + +describe("resolvePackageName", () => { + it("should return the base filename for a simple path", () => { + expect(resolvePackageName("my-package")).toBe("my-package"); + }); + + it("should return the base filename from a nested path", () => { + expect(resolvePackageName("some/path/my-package")).toBe("my-package"); + }); + + it("should return scoped package name when path starts with @", () => { + expect(resolvePackageName("@scope/my-package")).toBe("@scope/my-package"); + }); + + it("should return cwd basename when path is '.'", () => { + const expected = basename(process.cwd()); + expect(resolvePackageName(".")).toBe(expected); + }); +}); diff --git a/libs/schematics/src/utils/name.ts b/libs/schematics/src/utils/name.ts new file mode 100644 index 0000000..649feea --- /dev/null +++ b/libs/schematics/src/utils/name.ts @@ -0,0 +1,12 @@ +import { basename, parse } from "path"; + +export const resolvePackageName = (path: string): string => { + const { base: baseFilename, dir: dirname } = parse(path); + if (baseFilename === ".") { + return basename(process.cwd()); + } + if (dirname.match(/^@[^\s]/)) { + return `${dirname}/${baseFilename}`; + } + return baseFilename; +}; diff --git a/libs/schematics/src/utils/object.spec.ts b/libs/schematics/src/utils/object.spec.ts new file mode 100644 index 0000000..aa4bd25 --- /dev/null +++ b/libs/schematics/src/utils/object.spec.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; + +import { deepMerge, isObject } from "./object"; + +describe("isObject", () => { + it("should return true for a plain object", () => { + expect(isObject({})).toBe(true); + }); + + it("should return true for an object with properties", () => { + expect(isObject({ a: 1 })).toBe(true); + }); + + it("should return false for an array", () => { + expect(isObject([1, 2, 3])).toBe(false); + }); + + it("should return falsy for null", () => { + expect(isObject(null)).toBeFalsy(); + }); + + it("should return falsy for undefined", () => { + expect(isObject(undefined)).toBeFalsy(); + }); + + it("should return false for a string", () => { + expect(isObject("hello")).toBe(false); + }); + + it("should return false for a number", () => { + expect(isObject(42)).toBe(false); + }); +}); + +describe("deepMerge", () => { + it("should merge flat objects", () => { + expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 }); + }); + + it("should override values from source", () => { + expect(deepMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 }); + }); + + it("should deep merge nested objects", () => { + const target = { a: { b: 1, c: 2 } }; + const source = { a: { c: 3, d: 4 } }; + expect(deepMerge(target, source)).toEqual({ a: { b: 1, c: 3, d: 4 } }); + }); + + it("should handle multiple sources", () => { + expect(deepMerge({ a: 1 }, { b: 2 }, { c: 3 })).toEqual({ a: 1, b: 2, c: 3 }); + }); + + it("should return target when source is empty", () => { + const target = { a: 1 }; + expect(deepMerge(target, {})).toEqual({ a: 1 }); + }); + + it("should create nested keys in target if missing", () => { + expect(deepMerge({}, { a: { b: 1 } })).toEqual({ a: { b: 1 } }); + }); + + it("should not override a primitive with an object when target is not an object", () => { + expect(deepMerge({ a: 1 }, { a: { b: 2 } })).toEqual({ a: 1 }); + }); +}); diff --git a/libs/schematics/src/utils/object.ts b/libs/schematics/src/utils/object.ts new file mode 100644 index 0000000..b42a4e2 --- /dev/null +++ b/libs/schematics/src/utils/object.ts @@ -0,0 +1,21 @@ +export const isObject = (item: any) => { + return item && typeof item === "object" && !Array.isArray(item); +}; + +export const deepMerge = (target: any, ...sources: [any, ...any[]]): any => { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + deepMerge(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return deepMerge(target, ...sources); +}; diff --git a/libs/schematics/src/utils/path.ts b/libs/schematics/src/utils/path.ts new file mode 100644 index 0000000..ecd11a8 --- /dev/null +++ b/libs/schematics/src/utils/path.ts @@ -0,0 +1,6 @@ +import { join } from "path"; + +export const joinRelative = (...paths: [string, ...string[]]): string => { + const path = join(...paths); + return path.startsWith("./") || path.startsWith("../") ? path : `./${path}`; +}; diff --git a/libs/schematics/src/utils/registry.spec.ts b/libs/schematics/src/utils/registry.spec.ts new file mode 100644 index 0000000..a90034f --- /dev/null +++ b/libs/schematics/src/utils/registry.spec.ts @@ -0,0 +1,106 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { fetchTrustedVersion, fetchTrustedVersions } from "./registry"; + +const HOUR = 60 * 60 * 1000; + +const mockRegistryResponse = (time: Record, ok = true) => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue({ + ok, + json: () => Promise.resolve({ time }), + }), + ); +}; + +describe("fetchTrustedVersion", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-09-10T00:00:00Z")); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.useRealTimers(); + }); + + it("picks the newest version published at least 48h ago", async () => { + mockRegistryResponse({ + created: "2020-01-01T00:00:00Z", + modified: "2026-09-09T00:00:00Z", + "1.0.0": new Date(Date.now() - 100 * HOUR).toISOString(), + "1.1.0": new Date(Date.now() - 72 * HOUR).toISOString(), + "1.2.0": new Date(Date.now() - 1 * HOUR).toISOString(), // too fresh + }); + + const version = await fetchTrustedVersion("some-pkg", "0.0.0"); + expect(version).toBe("^1.1.0"); + }); + + it("excludes prerelease and non-semver tags", async () => { + mockRegistryResponse({ + "1.0.0": new Date(Date.now() - 100 * HOUR).toISOString(), + "1.1.0-beta.1": new Date(Date.now() - 100 * HOUR).toISOString(), + }); + + const version = await fetchTrustedVersion("some-pkg", "0.0.0"); + expect(version).toBe("^1.0.0"); + }); + + it("restricts to a requested major version", async () => { + mockRegistryResponse({ + "6.0.3": new Date(Date.now() - 100 * HOUR).toISOString(), + "7.0.2": new Date(Date.now() - 100 * HOUR).toISOString(), + }); + + const version = await fetchTrustedVersion("typescript", "6.0.0", { major: 6 }); + expect(version).toBe("^6.0.3"); + }); + + it("falls back when every version is too fresh", async () => { + mockRegistryResponse({ + "1.0.0": new Date(Date.now() - 1 * HOUR).toISOString(), + }); + + const version = await fetchTrustedVersion("some-pkg", "0.0.0"); + expect(version).toBe("0.0.0"); + }); + + it("falls back when the registry response is not ok", async () => { + mockRegistryResponse({}, false); + const version = await fetchTrustedVersion("missing-pkg", "0.0.0"); + expect(version).toBe("0.0.0"); + }); + + it("falls back when the fetch rejects", async () => { + vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("network down"))); + const version = await fetchTrustedVersion("some-pkg", "0.0.0"); + expect(version).toBe("0.0.0"); + }); +}); + +describe("fetchTrustedVersions", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-09-10T00:00:00Z")); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.useRealTimers(); + }); + + it("resolves multiple packages and keys the result by package name", async () => { + mockRegistryResponse({ + "2.5.0": new Date(Date.now() - 100 * HOUR).toISOString(), + }); + + const versions = await fetchTrustedVersions({ + "pkg-a": { fallback: "0.0.0" }, + "pkg-b": { fallback: "0.0.1" }, + }); + + expect(versions).toEqual({ "pkg-a": "^2.5.0", "pkg-b": "^2.5.0" }); + }); +}); diff --git a/libs/schematics/src/utils/registry.ts b/libs/schematics/src/utils/registry.ts new file mode 100644 index 0000000..e3de420 --- /dev/null +++ b/libs/schematics/src/utils/registry.ts @@ -0,0 +1,106 @@ +import { MIN_RELEASE_AGE_HOURS } from "~/defaults"; + +const REGISTRY_URL = "https://registry.npmjs.org"; +const DEFAULT_TIMEOUT_MS = 3000; + +const RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/; + +const compareVersions = (a: string, b: string): number => { + const partsA = a.split(".").map(Number); + const partsB = b.split(".").map(Number); + for (let i = 0; i < 3; i++) { + const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0); + if (diff !== 0) { + return diff; + } + } + return 0; +}; + +export interface FetchTrustedVersionOptions { + /** + * Only consider versions published at least this many hours ago, mirroring + * pnpm's `minimumReleaseAge` supply-chain protection for a freshly + * published (and potentially compromised) release. + * @default MIN_RELEASE_AGE_HOURS + */ + minAgeHours?: number; + + /** + * Restrict candidates to this major version (e.g. `6` only considers + * `6.x.x`). Useful to avoid silently jumping a generated project onto a + * new major release. + */ + major?: number; + + /** Network timeout in milliseconds. @default 3000 */ + timeoutMs?: number; +} + +/** + * Resolves the newest published version of an npm package that is at least + * `minAgeHours` old, so freshly (and potentially maliciously) published + * versions aren't picked up immediately. Falls back to `fallback` on any + * network error, timeout, missing package, or when no version satisfies the + * constraints, so callers never have to handle a rejected promise. + */ +export const fetchTrustedVersion = async ( + packageName: string, + fallback: string, + options: FetchTrustedVersionOptions = {}, +): Promise => { + const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options; + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, { + signal: controller.signal, + }); + if (!response.ok) { + return fallback; + } + + const data = (await response.json()) as { time?: Record }; + const time = data.time; + if (!time) { + return fallback; + } + + const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000; + const eligible = Object.entries(time) + .filter(([version]) => RELEASE_VERSION_PATTERN.test(version)) + .filter(([version]) => major === undefined || Number(version.split(".")[0]) === major) + .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff) + .map(([version]) => version) + .sort(compareVersions); + + return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback; + } catch { + return fallback; + } finally { + clearTimeout(timeout); + } +}; + +/** + * Resolves several packages' trusted versions in parallel. Keys of `specs` + * are the package name to query the registry for; values are the fallback + * to use if that lookup fails. Returns a map from the *same* keys to the + * resolved (or fallback) version — typed so callers don't get `| undefined` + * back for a key they just passed in. + */ +export const fetchTrustedVersions = async < + T extends Record, +>( + specs: T, +): Promise<{ [K in keyof T]: string }> => { + const entries = await Promise.all( + Object.entries(specs).map(async ([packageName, { fallback, major }]) => { + const version = await fetchTrustedVersion(packageName, fallback, { major }); + return [packageName, version] as const; + }), + ); + return Object.fromEntries(entries) as { [K in keyof T]: string }; +}; diff --git a/libs/schematics/src/utils/types.ts b/libs/schematics/src/utils/types.ts new file mode 100644 index 0000000..2f49720 --- /dev/null +++ b/libs/schematics/src/utils/types.ts @@ -0,0 +1,15 @@ +export type DeepPartial = + | T + | (T extends Date + ? T + : T extends Array + ? DeepPartial[] + : T extends Map + ? Map, DeepPartial> + : T extends Set + ? Set> + : T extends object + ? { + [K in keyof T]?: DeepPartial; + } + : T); diff --git a/libs/schematics/tsconfig.json b/libs/schematics/tsconfig.json new file mode 100644 index 0000000..af69dd6 --- /dev/null +++ b/libs/schematics/tsconfig.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig.json", + // Mapped from https://www.typescriptlang.org/tsconfig + "compilerOptions": { + // Type Checking + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "strict": true, + + // Modules + "allowArbitraryExtensions": false, + "allowImportingTsExtensions": false, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "resolvePackageJsonExports": true, + "resolvePackageJsonImports": true, + + // Emit + "declaration": true, + "declarationMap": true, + "importHelpers": false, + "newLine": "lf", + "noEmitHelpers": true, + "outDir": "dist", + "removeComments": false, + "sourceMap": true, + + // Interop Constraints + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + + // Language and Environment + "rootDir": ".", + "lib": ["ESNext", "esnext.disposable"], + "types": ["node"], + "target": "ESNext", + "experimentalDecorators": true, + "useDefineForClassFields": true, + "paths": { + "~/*": ["./src/*"], + "@utils/*": ["./src/utils/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "e2e", "src/libs/**/files/**/*", "src/**/*.spec.ts"] +} diff --git a/libs/schematics/tsdown.config.ts b/libs/schematics/tsdown.config.ts new file mode 100644 index 0000000..e779764 --- /dev/null +++ b/libs/schematics/tsdown.config.ts @@ -0,0 +1,41 @@ +import { type UserConfig, defineConfig } from "tsdown"; + +function createTsdownConfig({ + entry = ["src/index.ts"], + outDir = "dist", + format = ["esm", "cjs"], + shims = true, +}: UserConfig = {}) { + return defineConfig({ + entry, + outDir, + format, + shims, + dts: true, + fixedExtension: false, + platform: "node", + sourcemap: true, + target: "esnext", + treeshake: false, + deps: { + skipNodeModulesBundle: true, + }, + }); +} + +function createLibTsdownConfig(name: string) { + return createTsdownConfig({ + entry: [`src/libs/${name}/${name}.factory.ts`], + outDir: `dist/libs/${name}`, + format: ["esm"], + shims: false, + }); +} + +export default [ + createTsdownConfig(), + createLibTsdownConfig("workspace"), + createLibTsdownConfig("project"), + createLibTsdownConfig("component"), + createLibTsdownConfig("system"), +]; diff --git a/libs/schematics/vitest.config.e2e.ts b/libs/schematics/vitest.config.e2e.ts new file mode 100644 index 0000000..928e373 --- /dev/null +++ b/libs/schematics/vitest.config.e2e.ts @@ -0,0 +1,20 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "~": resolve(__dirname, "src"), + "@utils": resolve(__dirname, "src/utils"), + }, + }, + test: { + include: ["e2e/**/*.test.ts"], + passWithNoTests: true, + coverage: { + provider: "v8", + include: ["dist/**/*.js"], + exclude: ["dist/libs/**/files/**/*", "**/index.js"], + }, + }, +}); diff --git a/libs/schematics/vitest.config.ts b/libs/schematics/vitest.config.ts new file mode 100644 index 0000000..46a68f8 --- /dev/null +++ b/libs/schematics/vitest.config.ts @@ -0,0 +1,27 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "~": resolve(__dirname, "src"), + "@utils": resolve(__dirname, "src/utils"), + }, + }, + test: { + include: ["src/**/*.spec.ts"], + passWithNoTests: true, + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: [ + "src/**/*.spec.ts", + "src/libs/**/*", + "**/*.{interface,type,d}.ts", + "**/{interfaces,types}/*.ts", + "**/{interfaces,types}.ts", + "**/index.{js,ts}", + ], + }, + }, +}); diff --git a/package.json b/package.json index d653f47..82b1552 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,11 @@ "bugs": "https://github.com/NanoForge-dev/cli/issues", "license": "MIT", "contributors": [ - "Bill ", - "Exelo ", + "Exelo ", "Fexkoser ", - "Tchips " + "Harper ", + "Josephine ", + "Leo " ], "files": [ "dist" @@ -31,8 +32,15 @@ "type": "git", "url": "git+https://github.com/NanoForge-dev/CLI.git" }, - "funding": "", + "funding": "https://github.com/NanoForge-dev/CLI?sponsor", "scripts": { + "repo:build": "turbo run build --concurrency=5", + "repo:build:dev": "turbo run build:dev --concurrency=5", + "repo:lint": "turbo run lint --concurrency=5", + "repo:format": "turbo run format --concurrency=5", + "repo:test": "pnpm run repo:test:unit && pnpm run repo:test:e2e", + "repo:test:unit": "turbo run test:unit --concurrency=5", + "repo:test:e2e": "turbo run test:e2e --concurrency=5", "build": "tsc --noEmit && tsdown", "build:dev": "tsc --noEmit && NODE_ENV=development tsdown", "start": "node dist/nf.js", @@ -53,9 +61,11 @@ "@angular-devkit/schematics": "catalog:schematics", "@angular-devkit/schematics-cli": "catalog:schematics", "@inquirer/prompts": "catalog:cli", + "@nanoforge-dev/config": "workspace:*", + "@nanoforge-dev/editor": "catalog:core", "@nanoforge-dev/loader-client": "catalog:loader", "@nanoforge-dev/loader-server": "catalog:loader", - "@nanoforge-dev/schematics": "catalog:schematics", + "@nanoforge-dev/schematics": "workspace:*", "ansis": "catalog:cli", "bun": "catalog:core", "chokidar": "catalog:libs", @@ -79,21 +89,19 @@ "@trivago/prettier-plugin-sort-imports": "catalog:lint", "@types/inquirer": "catalog:cli", "@types/node": "catalog:core", - "@vitest/coverage-v8": "catalog:tests", + "@vitest/coverage-v8": "catalog:test", "eslint": "catalog:lint", "husky": "catalog:ci", "lint-staged": "catalog:ci", "prettier": "catalog:lint", "tsdown": "catalog:build", + "turbo": "catalog:build", "typescript": "catalog:build", - "vitest": "catalog:tests" + "vitest": "catalog:test" }, - "optionalDependencies": { - "@nanoforge-dev/editor": "catalog:core" - }, - "packageManager": "pnpm@11.10.0", + "packageManager": "pnpm@12.0.0", "engines": { - "node": "25" + "node": "26" }, "publishConfig": { "access": "public" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 949b5b0..8a79d9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,104 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.0.0 + version: 12.0.0 + +packages: + + '@pnpm/exe.darwin-arm64@12.0.0': + resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.0.0': + resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.0.0': + resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.0.0': + resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.0.0': + resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.0.0': + resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.0.0': + resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.0.0': + resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==} + cpu: [x64] + os: [win32] + + pnpm@12.0.0: + resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.0.0': + optional: true + + '@pnpm/exe.darwin-x64@12.0.0': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.0.0': + optional: true + + '@pnpm/exe.linux-arm64@12.0.0': + optional: true + + '@pnpm/exe.linux-x64-musl@12.0.0': + optional: true + + '@pnpm/exe.linux-x64@12.0.0': + optional: true + + '@pnpm/exe.win32-arm64@12.0.0': + optional: true + + '@pnpm/exe.win32-x64@12.0.0': + optional: true + + pnpm@12.0.0: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.0.0 + '@pnpm/exe.darwin-x64': 12.0.0 + '@pnpm/exe.linux-arm64': 12.0.0 + '@pnpm/exe.linux-arm64-musl': 12.0.0 + '@pnpm/exe.linux-x64': 12.0.0 + '@pnpm/exe.linux-x64-musl': 12.0.0 + '@pnpm/exe.win32-arm64': 12.0.0 + '@pnpm/exe.win32-x64': 12.0.0 + +--- lockfileVersion: '9.0' settings: @@ -6,19 +107,25 @@ settings: catalogs: build: + cpx2: + specifier: ^9.0.0 + version: 9.0.3 tsdown: - specifier: ^0.22.3 - version: 0.22.3 + specifier: ^0.23.0 + version: 0.23.0 + turbo: + specifier: ^2.11.3 + version: 2.11.3 typescript: specifier: ^6.0.3 version: 6.0.3 ci: '@commitlint/cli': - specifier: ^21.2.0 - version: 21.2.0 + specifier: ^21.2.3 + version: 21.2.3 '@commitlint/config-conventional': - specifier: ^21.2.0 - version: 21.2.0 + specifier: ^21.2.3 + version: 21.2.3 '@favware/cliff-jumper': specifier: ^6.1.0 version: 6.1.0 @@ -29,18 +136,18 @@ catalogs: specifier: ^9.1.7 version: 9.1.7 lint-staged: - specifier: ^17.0.8 - version: 17.0.8 + specifier: ^17.5.1 + version: 17.5.1 cli: '@inquirer/prompts': - specifier: ^8.5.2 - version: 8.5.2 + specifier: ^8.7.2 + version: 8.7.2 '@types/inquirer': specifier: ^9.0.10 version: 9.0.10 ansis: - specifier: ^4.3.1 - version: 4.3.1 + specifier: ^4.4.0 + version: 4.4.0 commander: specifier: ^15.0.0 version: 15.0.0 @@ -55,11 +162,11 @@ catalogs: specifier: ^0.1.4 version: 0.1.4 '@types/node': - specifier: ^26.1.0 - version: 26.1.0 + specifier: ^26.6.2 + version: 26.6.2 bun: - specifier: ^1.3.14 - version: 1.3.14 + specifier: ^1.4.2 + version: 1.4.2 libs: chokidar: specifier: ^5.0.0 @@ -71,17 +178,23 @@ catalogs: specifier: ^0.15.1 version: 0.15.1 dotenv: - specifier: ^17.4.2 - version: 17.4.2 + specifier: ^18.0.3 + version: 18.0.3 open: - specifier: ^11.0.0 - version: 11.0.0 + specifier: ^11.0.4 + version: 11.0.4 rc9: - specifier: ^3.0.1 - version: 3.0.1 + specifier: ^3.1.0 + version: 3.1.0 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 + unrun: + specifier: ^0.3.0 + version: 0.3.1 + zod: + specifier: ^4.5.4 + version: 4.6.5 lint: '@nanoforge-dev/utils-eslint-config': specifier: ^1.0.2 @@ -93,35 +206,38 @@ catalogs: specifier: ^6.0.2 version: 6.0.2 eslint: - specifier: ^10.6.0 - version: 10.6.0 + specifier: ^10.11.0 + version: 10.11.0 prettier: - specifier: ^3.9.4 - version: 3.9.4 + specifier: ^3.9.9 + version: 3.9.9 loader: '@nanoforge-dev/loader-client': - specifier: ^1.4.0 - version: 1.4.0 + specifier: ^1.5.1 + version: 1.5.1 '@nanoforge-dev/loader-server': - specifier: ^1.4.0 - version: 1.4.0 + specifier: ^1.5.1 + version: 1.5.1 schematics: + '@angular-devkit/core': + specifier: ^22.0.4 + version: 22.1.8 '@angular-devkit/schematics': - specifier: ^22.0.5 - version: 22.0.5 + specifier: ^22.1.8 + version: 22.1.8 '@angular-devkit/schematics-cli': - specifier: ^22.0.5 - version: 22.0.5 - '@nanoforge-dev/schematics': - specifier: ^2.2.1 - version: 2.2.1 - tests: + specifier: ^22.1.8 + version: 22.1.8 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + test: '@vitest/coverage-v8': - specifier: ^4.1.10 - version: 4.1.10 + specifier: ^5.0.1 + version: 5.0.1 vitest: - specifier: ^4.1.10 - version: 4.1.10 + specifier: ^5.0.1 + version: 5.0.1 importers: @@ -129,28 +245,34 @@ importers: dependencies: '@angular-devkit/schematics': specifier: catalog:schematics - version: 22.0.5(chokidar@5.0.0) + version: 22.1.8(chokidar@5.0.0) '@angular-devkit/schematics-cli': specifier: catalog:schematics - version: 22.0.5(@types/node@26.1.0)(chokidar@5.0.0) + version: 22.1.8(@types/node@26.6.2)(chokidar@5.0.0) '@inquirer/prompts': specifier: catalog:cli - version: 8.5.2(@types/node@26.1.0) + version: 8.7.2(@types/node@26.6.2) + '@nanoforge-dev/config': + specifier: workspace:* + version: link:libs/config + '@nanoforge-dev/editor': + specifier: catalog:core + version: 0.1.4 '@nanoforge-dev/loader-client': specifier: catalog:loader - version: 1.4.0 + version: 1.5.1 '@nanoforge-dev/loader-server': specifier: catalog:loader - version: 1.4.0 + version: 1.5.1 '@nanoforge-dev/schematics': - specifier: catalog:schematics - version: 2.2.1(chokidar@5.0.0) + specifier: workspace:* + version: link:libs/schematics ansis: specifier: catalog:cli - version: 4.3.1 + version: 4.4.0 bun: specifier: catalog:core - version: 1.3.14 + version: 1.4.2 chokidar: specifier: catalog:libs version: 5.0.0 @@ -165,29 +287,29 @@ importers: version: 15.0.0 dotenv: specifier: catalog:libs - version: 17.4.2 + version: 18.0.3 node-emoji: specifier: catalog:cli version: 2.2.0 open: specifier: catalog:libs - version: 11.0.0 + version: 11.0.4 ora: specifier: catalog:cli version: 9.4.1 rc9: specifier: catalog:libs - version: 3.0.1 + version: 3.1.0 reflect-metadata: specifier: catalog:libs version: 0.2.2 devDependencies: '@commitlint/cli': specifier: catalog:ci - version: 21.2.0(@types/node@26.1.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3) + version: 21.2.3(@types/node@26.6.2)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3) '@commitlint/config-conventional': specifier: catalog:ci - version: 21.2.0 + version: 21.2.3 '@favware/cliff-jumper': specifier: catalog:ci version: 6.1.0 @@ -196,47 +318,150 @@ importers: version: 2.1.5 '@nanoforge-dev/utils-eslint-config': specifier: catalog:lint - version: 1.0.2(@types/eslint@9.6.1)(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4)(typescript@6.0.3) + version: 1.0.2(@types/eslint@9.6.1)(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.9)(supports-color@10.2.2)(typescript@6.0.3) '@nanoforge-dev/utils-prettier-config': specifier: catalog:lint version: 1.0.2 '@trivago/prettier-plugin-sort-imports': specifier: catalog:lint - version: 6.0.2(prettier@3.9.4) + version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2) '@types/inquirer': specifier: catalog:cli version: 9.0.10 '@types/node': specifier: catalog:core - version: 26.1.0 + version: 26.6.2 '@vitest/coverage-v8': - specifier: catalog:tests - version: 4.1.10(vitest@4.1.10) + specifier: catalog:test + version: 5.0.1(vitest@5.0.1) eslint: specifier: catalog:lint - version: 10.6.0(jiti@2.6.1) + version: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) husky: specifier: catalog:ci version: 9.1.7 lint-staged: specifier: catalog:ci - version: 17.0.8 + version: 17.5.1 prettier: specifier: catalog:lint - version: 3.9.4 + version: 3.9.9 tsdown: specifier: catalog:build - version: 0.22.3(typescript@6.0.3) + version: 0.23.0(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)) + turbo: + specifier: catalog:build + version: 2.11.3 typescript: specifier: catalog:build version: 6.0.3 vitest: - specifier: catalog:tests - version: 4.1.10(@types/node@26.1.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)) - optionalDependencies: - '@nanoforge-dev/editor': + specifier: catalog:test + version: 5.0.1(@types/node@26.6.2)(@vitest/coverage-v8@5.0.1)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1)) + + libs/config: + dependencies: + unrun: + specifier: catalog:libs + version: 0.3.1(synckit@0.11.13) + zod: + specifier: catalog:libs + version: 4.6.5 + devDependencies: + '@favware/cliff-jumper': + specifier: catalog:ci + version: 6.1.0 + '@nanoforge-dev/utils-eslint-config': + specifier: catalog:lint + version: 1.0.2(@types/eslint@9.6.1)(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.9)(supports-color@10.2.2)(typescript@6.0.3) + '@nanoforge-dev/utils-prettier-config': + specifier: catalog:lint + version: 1.0.2 + '@trivago/prettier-plugin-sort-imports': + specifier: catalog:lint + version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2) + eslint: + specifier: catalog:lint + version: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) + prettier: + specifier: catalog:lint + version: 3.9.9 + tsdown: + specifier: catalog:build + version: 0.23.0(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)) + typescript: + specifier: catalog:build + version: 6.0.3 + vitest: + specifier: catalog:test + version: 5.0.1(@types/node@26.6.2)(@vitest/coverage-v8@5.0.1)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1)) + + libs/schematics: + dependencies: + '@angular-devkit/core': + specifier: catalog:schematics + version: 22.1.8(chokidar@5.0.0) + '@angular-devkit/schematics': + specifier: catalog:schematics + version: 22.1.8(chokidar@5.0.0) + rxjs: + specifier: catalog:schematics + version: 7.8.2 + devDependencies: + '@angular-devkit/schematics-cli': + specifier: catalog:schematics + version: 22.1.8(@types/node@26.6.2)(chokidar@5.0.0) + '@commitlint/cli': + specifier: catalog:ci + version: 21.2.3(@types/node@26.6.2)(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3) + '@commitlint/config-conventional': + specifier: catalog:ci + version: 21.2.3 + '@favware/cliff-jumper': + specifier: catalog:ci + version: 6.1.0 + '@nanoforge-dev/actions': + specifier: catalog:ci + version: 2.1.5 + '@nanoforge-dev/utils-eslint-config': + specifier: catalog:lint + version: 1.0.2(@types/eslint@9.6.1)(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.9)(supports-color@10.2.2)(typescript@6.0.3) + '@nanoforge-dev/utils-prettier-config': + specifier: catalog:lint + version: 1.0.2 + '@trivago/prettier-plugin-sort-imports': + specifier: catalog:lint + version: 6.0.2(prettier@3.9.9)(supports-color@10.2.2) + '@types/node': specifier: catalog:core - version: 0.1.4 + version: 26.6.2 + '@vitest/coverage-v8': + specifier: catalog:test + version: 5.0.1(vitest@5.0.1) + cpx2: + specifier: catalog:build + version: 9.0.3 + eslint: + specifier: catalog:lint + version: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) + husky: + specifier: catalog:ci + version: 9.1.7 + lint-staged: + specifier: catalog:ci + version: 17.5.1 + prettier: + specifier: catalog:lint + version: 3.9.9 + tsdown: + specifier: catalog:build + version: 0.23.0(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)) + typescript: + specifier: catalog:build + version: 6.0.3 + vitest: + specifier: catalog:test + version: 5.0.1(@types/node@26.6.2)(@vitest/coverage-v8@5.0.1)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1)) packages: @@ -258,8 +483,8 @@ packages: '@actions/io@3.0.2': resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==} - '@angular-devkit/core@22.0.5': - resolution: {integrity: sha512-xxd3b9X0bbdGuYqDj1OgxtlGotrb/gsxQ5+4aqivWBHq4q/1RI8JfVB7gqcoYTvAfTq63Dwsk7Qd33hC4yJ1Wg==} + '@angular-devkit/core@22.1.8': + resolution: {integrity: sha512-34lsgg2FwMVBX7nR/ZqzRUcdvdYPvSB8XAEr2GudXAyZwLI9ehzfQlagAWR/gEb8p4uCFZW0r7uU0toxydq4Rw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^5.0.0 @@ -267,27 +492,23 @@ packages: chokidar: optional: true - '@angular-devkit/schematics-cli@22.0.5': - resolution: {integrity: sha512-X305DwlHLyVE+RoM2C9KjKvShi2FiS4pkjYH6CvfydUmtyVxGj34kgtIPf8Kk+Jn7St7fRja5IUAcxczBgL3SQ==} + '@angular-devkit/schematics-cli@22.1.8': + resolution: {integrity: sha512-SsGG8ompVzprmYE9sHi2L7DYZ+i1sD+YDfun6Rg2vkTCusejtD4SSGVxwuR6++dT22ZpN16OqUTmii/0bhE4xA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/schematics@22.0.5': - resolution: {integrity: sha512-yKkImZuFLIbylmk1REyoW3EWQeqnr6qwtrzs4EPeaJgzjj+MZurq5slQ3fanK9lfX2q+W+dnSQFKBXHxlUGJ8Q==} + '@angular-devkit/schematics@22.1.8': + resolution: {integrity: sha512-Pv3cPa/44kvEwYcqwx4Ns5dhIDmcFNSHhbpheqiEG1Z/u4e2t4zHKDtE3eHZB+8o+IcC7xJj+d+AqGR44RoZDA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} - '@babel/generator@8.0.0': - resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-globals@7.29.7': resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} @@ -296,103 +517,92 @@ packages: resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@8.0.0': - resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} - engines: {node: ^22.18.0 || >=24.11.0} - '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@8.0.2': - resolution: {integrity: sha512-9Fr9QeyCAyi1BR1jKZ6uYQ24EIhQUx5ReHfQU7drOE+TPOb+w11/dsqLkMOT2U29OdCT71XajrOT8xDc1C7orA==} - engines: {node: ^22.18.0 || >=24.11.0} - - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/parser@8.0.0': - resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==} - engines: {node: ^22.18.0 || >=24.11.0} - hasBin: true - '@babel/template@7.29.7': resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} - '@babel/types@8.0.0': - resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==} - engines: {node: ^22.18.0 || >=24.11.0} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@commitlint/cli@21.2.0': - resolution: {integrity: sha512-4GLVIhUaT3c3GBlQ0GB80/5H3xXdn/Tgw4lrsuoOQVDu2wl4Xw0GuzSar8xZKSMv4H3xaKaQXmvH91GmdyYBZA==} + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@commitlint/cli@21.2.3': + resolution: {integrity: sha512-5yOX6IRjcrIReCxpyAd04hgI/yhUMpVBJ/ouwFV0wk9yx5+fYRDWZWJ5h55Tbedgfol+5m2mU5nqzbB8g3v0vQ==} engines: {node: '>=22.12.0'} hasBin: true - '@commitlint/config-conventional@21.2.0': - resolution: {integrity: sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==} + '@commitlint/config-conventional@21.2.3': + resolution: {integrity: sha512-EP2n5DN2GwRWNVrLX0Jx3mpQlW5PfFi9HnD9OJ8hN/H0xFqpCFyHeGDhg+0P8/21iMMcdkcQgbNqmVklebqYsQ==} engines: {node: '>=22.12.0'} - '@commitlint/config-validator@21.2.0': - resolution: {integrity: sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==} + '@commitlint/config-validator@21.2.3': + resolution: {integrity: sha512-JQ13v0GafSM4fBYX8UowSy++07Was20auq0NcTdnzSEq6/05Cq5GBUMIfKkqjzyBwC9sJFmPpKWgLLhH7cygkw==} engines: {node: '>=22.12.0'} - '@commitlint/ensure@21.2.0': - resolution: {integrity: sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==} + '@commitlint/ensure@21.2.3': + resolution: {integrity: sha512-gEjNKgRocbODI5/sMx2Qc9XOf2/zFTNV2aEK1QZ4gKDKxDC/VR7us3QtIX1C5d5xxHxx0I/tEbYbdSP1Am2LwA==} engines: {node: '>=22.12.0'} '@commitlint/execute-rule@21.0.1': resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==} engines: {node: '>=22.12.0'} - '@commitlint/format@21.2.0': - resolution: {integrity: sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==} + '@commitlint/format@21.2.3': + resolution: {integrity: sha512-KKwCjXYqA8r295uhLnulDXL9yekdXmj2yp2Q0XsBPhxQ2iPuvRE8F43j7g7yexo69wTdmUQmFz15jpderluzUQ==} engines: {node: '>=22.12.0'} - '@commitlint/is-ignored@21.2.0': - resolution: {integrity: sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==} + '@commitlint/is-ignored@21.2.3': + resolution: {integrity: sha512-Ng4nRj/LBAOvc+GQu7bsFW4/fL89LvZ14OOC/ERLbUpnG6QBwWRzY/1oXGwPs3C8YHWjJObin4kLYo2YeEn70w==} engines: {node: '>=22.12.0'} - '@commitlint/lint@21.2.0': - resolution: {integrity: sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==} + '@commitlint/lint@21.2.3': + resolution: {integrity: sha512-lUZcSuVLLEDkhge9ERk8Rp+FXQtrUhCK+h328BFnzKXrTuXGdqiYMEpTlNWdZaOrjyMhMJaz354ZaWEGJxcLeg==} engines: {node: '>=22.12.0'} - '@commitlint/load@21.2.0': - resolution: {integrity: sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==} + '@commitlint/load@21.2.3': + resolution: {integrity: sha512-cDrL8lOo23Lk7knGzQdjbFcq7z2JgvjwV6PhbESVvEc+Fe21MeLjPKBJi8nKjg7j+jh79CVbVSUbZc104thXRw==} engines: {node: '>=22.12.0'} '@commitlint/message@21.2.0': resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==} engines: {node: '>=22.12.0'} - '@commitlint/parse@21.2.0': - resolution: {integrity: sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==} + '@commitlint/parse@21.2.3': + resolution: {integrity: sha512-Jt61QpF1xA88damCELe8I0FlDZPSpXNc8IhN7z5kZ0EcDYPjdZ+ZHPvsSXl69r2MIlLnNKeHhOn2cmVpziFE9w==} engines: {node: '>=22.12.0'} - '@commitlint/read@21.2.0': - resolution: {integrity: sha512-ELx8Ovh/JoAw5lpvDgxc6Y0We9skf2IPI2RFN+gnYgDGjRdMSF8zeodxhZmcclLWzfUIF7hXLOa8gOlllYcvBA==} + '@commitlint/read@21.2.3': + resolution: {integrity: sha512-szAepuLA1vTTr7fuudEg7jYyVHudf+FH+vptU91bhxE1WnE8TKrfTAi44HVllz8XpI8Pvp4GjR/iP5Nshgh+og==} engines: {node: '>=22.12.0'} - '@commitlint/resolve-extends@21.2.0': - resolution: {integrity: sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==} + '@commitlint/resolve-extends@21.2.3': + resolution: {integrity: sha512-bqJdmAtXgAQel+l02uIsF7rHgKgx1AGZdGBjam7uK4XW54ZAQtB6XAIUXezYprQiCcRMpBh4fHs5JodXK4mVZA==} engines: {node: '>=22.12.0'} - '@commitlint/rules@21.2.0': - resolution: {integrity: sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==} + '@commitlint/rules@21.2.3': + resolution: {integrity: sha512-5+YnI/LuTx3JDl9GjqfP2z9x96M8KKDrd+4jOel/T0L1BB3oAmBlSN8xppf63QsudiVEgR8C0gRoAMrPVrms6g==} engines: {node: '>=22.12.0'} '@commitlint/to-lines@21.0.1': @@ -403,8 +613,8 @@ packages: resolution: {integrity: sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==} engines: {node: '>=22.12.0'} - '@commitlint/types@21.2.0': - resolution: {integrity: sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==} + '@commitlint/types@21.2.3': + resolution: {integrity: sha512-sGEA473TlCvhCGNoTBY9OXZVWdscIXdGt0dm8EpB+2MRfmt8LNh/X56P/T3nCtEB+eH5qGEa3odphSAoUxSfSQ==} engines: {node: '>=22.12.0'} '@conventional-changelog/git-client@2.7.0': @@ -419,8 +629,20 @@ packages: conventional-commits-parser: optional: true - '@conventional-changelog/template@1.2.1': - resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==} + '@conventional-changelog/git-client@3.1.2': + resolution: {integrity: sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==} + engines: {node: '>=22'} + peerDependencies: + conventional-commits-filter: ^6.0.1 + conventional-commits-parser: ^7.1.2 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + + '@conventional-changelog/template@1.4.0': + resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} engines: {node: '>=22'} '@dprint/formatter@0.5.1': @@ -432,17 +654,8 @@ packages: '@dprint/toml@0.7.0': resolution: {integrity: sha512-eFaQTcfxKHB+YyTh83x7GEv+gDPuj9q5NFOTaoj5rZmQTbj6OgjjMxUicmS1R8zYcx8YAq5oA9J3YFa5U6x2gA==} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -455,24 +668,24 @@ packages: resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/js@9.39.4': - resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@3.0.5': resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@favware/cliff-jumper@6.1.0': @@ -504,12 +717,12 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@inquirer/ansi@2.0.7': - resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + '@inquirer/ansi@2.0.8': + resolution: {integrity: sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/checkbox@5.2.1': - resolution: {integrity: sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==} + '@inquirer/checkbox@5.2.5': + resolution: {integrity: sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -517,8 +730,8 @@ packages: '@types/node': optional: true - '@inquirer/confirm@6.1.1': - resolution: {integrity: sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==} + '@inquirer/confirm@6.3.2': + resolution: {integrity: sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -526,8 +739,8 @@ packages: '@types/node': optional: true - '@inquirer/core@11.2.1': - resolution: {integrity: sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==} + '@inquirer/core@12.0.3': + resolution: {integrity: sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -535,8 +748,8 @@ packages: '@types/node': optional: true - '@inquirer/editor@5.2.2': - resolution: {integrity: sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==} + '@inquirer/editor@5.3.3': + resolution: {integrity: sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -544,8 +757,8 @@ packages: '@types/node': optional: true - '@inquirer/expand@5.1.1': - resolution: {integrity: sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==} + '@inquirer/expand@5.1.5': + resolution: {integrity: sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -553,8 +766,8 @@ packages: '@types/node': optional: true - '@inquirer/external-editor@3.0.3': - resolution: {integrity: sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==} + '@inquirer/external-editor@3.0.5': + resolution: {integrity: sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -562,12 +775,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@2.0.7': - resolution: {integrity: sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==} + '@inquirer/figures@2.0.9': + resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/input@5.1.2': - resolution: {integrity: sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==} + '@inquirer/input@5.1.6': + resolution: {integrity: sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -575,8 +788,8 @@ packages: '@types/node': optional: true - '@inquirer/number@4.1.1': - resolution: {integrity: sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==} + '@inquirer/number@4.2.3': + resolution: {integrity: sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -584,8 +797,8 @@ packages: '@types/node': optional: true - '@inquirer/password@5.1.1': - resolution: {integrity: sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==} + '@inquirer/password@5.2.2': + resolution: {integrity: sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -593,17 +806,17 @@ packages: '@types/node': optional: true - '@inquirer/prompts@8.4.2': - resolution: {integrity: sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} + '@inquirer/prompts@8.5.2': + resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' peerDependenciesMeta: '@types/node': optional: true - '@inquirer/prompts@8.5.2': - resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} + '@inquirer/prompts@8.7.2': + resolution: {integrity: sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -611,8 +824,8 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@5.3.1': - resolution: {integrity: sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==} + '@inquirer/rawlist@5.3.5': + resolution: {integrity: sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -620,8 +833,8 @@ packages: '@types/node': optional: true - '@inquirer/search@4.2.1': - resolution: {integrity: sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==} + '@inquirer/search@4.3.3': + resolution: {integrity: sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -629,8 +842,8 @@ packages: '@types/node': optional: true - '@inquirer/select@5.2.1': - resolution: {integrity: sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==} + '@inquirer/select@5.2.5': + resolution: {integrity: sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -638,8 +851,8 @@ packages: '@types/node': optional: true - '@inquirer/type@4.0.7': - resolution: {integrity: sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==} + '@inquirer/type@4.1.1': + resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -658,12 +871,21 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@nanoforge-dev/actions@2.1.5': resolution: {integrity: sha512-aXFw1PGS8NmSEGAIJ1g5X+0kAiaWYPOvxkLttmKR1qO95O17mcpkKXfTdoSIyFnqPF2YnZjq9KR7Cvs27fNewg==} engines: {node: '25'} @@ -672,21 +894,17 @@ packages: resolution: {integrity: sha512-bqbrH8ulFSiZz1iisJHtHeEzj1RV/70uErCYHeW4DIhVPsWbBfWKFvg/ghAbmPk5vsj0YnhDX6DSpik4S1QoiQ==} engines: {node: '25'} - '@nanoforge-dev/loader-client@1.4.0': - resolution: {integrity: sha512-YYE41uqp/7+IXRXLi71UuWFBUXf8ZguMf32A/8qzCmbQN+gUtBdTKLwm+2VVcvih6mueCQCGnulH9t5SmC8+Qg==} - engines: {node: '25'} + '@nanoforge-dev/loader-client@1.5.1': + resolution: {integrity: sha512-1PueGpj8DbjRcZTVVCKAkKi4A8gyBaFTkfp78fFrW47J0HZcfS8pq+AaNaBjEUOozn0E7L+U8zGf/5laxTRe1A==} + engines: {node: '26'} - '@nanoforge-dev/loader-server@1.4.0': - resolution: {integrity: sha512-jvNoHxx54BAfjUTMZHb/T/8ApbdPV/NNGwiQITUBprH4Uv8jQmbR7zNB5Z6/vIEjSVVxrU0b4fFa+BkdnKX/iQ==} - engines: {node: '25'} - - '@nanoforge-dev/loader-website@1.4.0': - resolution: {integrity: sha512-+5el/zkSJQL9qCd+4a6fGpIDMQi26QRTWXJ24mJFMaDC74P1lvqnes0u0l0+/s9dunJxfgea0Epse0/Jj/3M3Q==} - engines: {node: '25'} + '@nanoforge-dev/loader-server@1.5.1': + resolution: {integrity: sha512-RNqA0zpEhUXENKu5chJ+vQOhr8qK+a7z6XePmiCJygpSScJ8p1dlWquFYNwu9dBLHBP8BhK9dSFvXjPvHAYBxQ==} + engines: {node: '26'} - '@nanoforge-dev/schematics@2.2.1': - resolution: {integrity: sha512-MdmhFncakgejGRIf1HTPKTskhdv8bIst21uWRSZCXGgGv0UNYEfDiziOsWPVCzGc9dXei59RIDznBwgpAMeXLg==} - engines: {node: '25'} + '@nanoforge-dev/loader-website@1.5.1': + resolution: {integrity: sha512-Ugu5CvfbsCK2wG8RXBHkkVlNHkSlXz2xXxfqglID7Cb4E8PHO0p1hbE6e7+1fWXAdFhEivGQHSI1POIvgyP1Qg==} + engines: {node: '26'} '@nanoforge-dev/utils-eslint-config@1.0.2': resolution: {integrity: sha512-hIsVGGpVIW3RvPZocWEf15Y5eqmIxTtLfWI66wqx+AO6PHcatUBhEi4sk1i54Qb2yGFueRBS/dSXj0kxG4Rbcw==} @@ -696,31 +914,31 @@ packages: resolution: {integrity: sha512-dTa2ixPJmWVD+L13Iuxx51JIm3gaX+gnRCe8jA0PFw0xjMjLYv+0ZgbWSSVlNmowp6fqpOzfPdWADkkPNQs8LQ==} engines: {node: '25'} - '@napi-rs/wasm-runtime@1.1.6': - resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} - '@octokit/core@7.0.6': - resolution: {integrity: sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==} + '@octokit/core@7.0.8': + resolution: {integrity: sha512-L7y8eYc+AwxGr2PWI4WFt1VG4TiJ66c26BD16mXpYIlXxG0SMigM1+m4aTSlYyBr5BlQsGAlz8uDCoZN4SEMcg==} engines: {node: '>= 20'} - '@octokit/endpoint@11.0.3': - resolution: {integrity: sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==} + '@octokit/endpoint@11.0.5': + resolution: {integrity: sha512-iXa654H3yFafF/ieHkukfbgWo2rmXD2ceD0ZOtrPhw1bc3FDch1d9N/TNs0FQ1/cIbwb7kspUX8jzIs8nzb9DQ==} engines: {node: '>= 20'} - '@octokit/graphql@9.0.3': - resolution: {integrity: sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==} + '@octokit/graphql@9.0.5': + resolution: {integrity: sha512-bt/hm03LeU6Vy7FwTrkkC9p3XGT/lBwClglMqxBSe5/q0E5CdJTXeAqEI0vlw89/LF/G6tryTIH8HirZ3prMVg==} engines: {node: '>= 20'} '@octokit/openapi-types@27.0.0': resolution: {integrity: sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==} + '@octokit/openapi-types@28.0.0': + resolution: {integrity: sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==} + + '@octokit/openapi-types@29.0.1': + resolution: {integrity: sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==} + '@octokit/plugin-paginate-rest@14.0.0': resolution: {integrity: sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==} engines: {node: '>= 20'} @@ -733,105 +951,95 @@ packages: peerDependencies: '@octokit/core': '>=6' - '@octokit/plugin-retry@8.1.0': - resolution: {integrity: sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==} + '@octokit/plugin-retry@8.1.1': + resolution: {integrity: sha512-VCVvZ/R1+u3WuiBWpNavZ0mY4aaJNAsENrpBP9aLSR2QyOpQgd7DhM5j4AW7z4MQpnJYgwBPf0XqPQoNBRdQwg==} engines: {node: '>= 20'} peerDependencies: '@octokit/core': '>=7' - '@octokit/request-error@7.1.0': - resolution: {integrity: sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==} + '@octokit/request-error@7.1.2': + resolution: {integrity: sha512-XZRuT3xZ84D3gYErI1DZvhJ33dCWVV6uzBtWkaBB4TvA/L6eOeTZodxLFVB44bBEEo3vEx7y00UfX1tBLrtLRg==} engines: {node: '>= 20'} - '@octokit/request@10.0.11': - resolution: {integrity: sha512-+s7HUxjfFqOMS9VlIwDffq0MikjSAK0gSpG73W+meAvVAvX4MBrHYTK5Bj3Uot55qFT4gzUtfzE4mGWY4Br8/Q==} + '@octokit/request@10.0.16': + resolution: {integrity: sha512-A0zWGjHzISIb+9ccG8s0dq7LKO5zVpJLRICjgUb+sJxEWqn8RUHB1rD3AE51+PECvXHIxqZ1VVvs4fHTSD9nUQ==} engines: {node: '>= 20'} '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} - '@oven/bun-darwin-aarch64@1.3.14': - resolution: {integrity: sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A==} - cpu: [arm64] - os: [darwin] + '@octokit/types@17.0.0': + resolution: {integrity: sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==} - '@oven/bun-darwin-x64-baseline@1.3.14': - resolution: {integrity: sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g==} - cpu: [x64] + '@octokit/types@18.0.0': + resolution: {integrity: sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==} + + '@oven/bun-darwin-aarch64@1.4.2': + resolution: {integrity: sha512-MXdZkP1featqxZ+/VTXWG1BVjM4OGBehVY2Q88EeUj/7L0UMeCGItmyPYTN+wxvlGJ6F66JEtzsw+GvQWewnag==} + cpu: [arm64] os: [darwin] - '@oven/bun-darwin-x64@1.3.14': - resolution: {integrity: sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w==} + '@oven/bun-darwin-x64@1.4.2': + resolution: {integrity: sha512-gZTxZuLjkUhAWjTETu3tw0WhsEdNkJ64daj60ybhPf835a2yollV3yTkK9JozvzKPx4TRFzLSl8C+U525pxVbw==} cpu: [x64] os: [darwin] - '@oven/bun-freebsd-aarch64@1.3.14': - resolution: {integrity: sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw==} + '@oven/bun-freebsd-aarch64@1.4.2': + resolution: {integrity: sha512-SMNItMw1Z8QeeQVKnw8jA7xQNkeXdP+OPgin4Wi/QTx/B8RHHLnuZfqmFy7NtVeT2NF0kKYppW4WWd2CCYZjhQ==} cpu: [arm64] os: [freebsd] - '@oven/bun-freebsd-x64@1.3.14': - resolution: {integrity: sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw==} + '@oven/bun-freebsd-x64@1.4.2': + resolution: {integrity: sha512-THbPKXhO54N0DpFRKZNDZpQ7dpbX0bWASuARckAUS9wRtFIHsiY+uULXJvxJGo2YD1YewvXQ4G8Fj7XT5oBCiw==} cpu: [x64] os: [freebsd] - '@oven/bun-linux-aarch64-android@1.3.14': - resolution: {integrity: sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA==} + '@oven/bun-linux-aarch64-android@1.4.2': + resolution: {integrity: sha512-3mZKO2rhsNgbAUtAHC1UKUlF2zTxFraDZT/Elv8wzyH0fJL9h+Iv3TgB9lO63w89PRn3eFe+NRA1bhVgikKNPQ==} cpu: [arm64] os: [android] - '@oven/bun-linux-aarch64-musl@1.3.14': - resolution: {integrity: sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA==} + '@oven/bun-linux-aarch64-musl@1.4.2': + resolution: {integrity: sha512-+Sm6y+lSiSFBOtXmnekp5Q6n1tUKlyv71FCPWBc61Cgb14T5eBs8SN/nh4MUCOKzONkI3O+as3MGUgikS4aCBQ==} cpu: [arm64] os: [linux] + libc: [musl] - '@oven/bun-linux-aarch64@1.3.14': - resolution: {integrity: sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw==} + '@oven/bun-linux-aarch64@1.4.2': + resolution: {integrity: sha512-3BBP9ovJ2RGHFH6Ae1CAtxNtG1+YY6GD6rmYbsUosoAk9+OEl6zeDQ/k4fBkc6dYOJCtWnx8hUxzNzQATSmvYQ==} cpu: [arm64] os: [linux] + libc: [glibc] - '@oven/bun-linux-x64-android@1.3.14': - resolution: {integrity: sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg==} + '@oven/bun-linux-x64-android@1.4.2': + resolution: {integrity: sha512-6HC5tzcC79113n2IHCTJMWv+HsQImv4ZFEK2XpYLxY6HbT8tM4cUM2Zv1bHZBQsS3jv/zYBamDJ1UX7If0d5tw==} cpu: [x64] os: [android] - '@oven/bun-linux-x64-baseline@1.3.14': - resolution: {integrity: sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA==} - cpu: [x64] - os: [linux] - - '@oven/bun-linux-x64-musl-baseline@1.3.14': - resolution: {integrity: sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw==} - cpu: [x64] - os: [linux] - - '@oven/bun-linux-x64-musl@1.3.14': - resolution: {integrity: sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg==} + '@oven/bun-linux-x64-musl@1.4.2': + resolution: {integrity: sha512-vVTKUg1bnPhRP/Hp73jIVoFh2vPFNYEqYX0ERKfZBOQEEHitNAeukZzzuUDZS0SoDCIpuWUGSpd/CDMbjdR+Uw==} cpu: [x64] os: [linux] + libc: [musl] - '@oven/bun-linux-x64@1.3.14': - resolution: {integrity: sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw==} + '@oven/bun-linux-x64@1.4.2': + resolution: {integrity: sha512-9/E/UXOTpSo3YsV5g+FhtTd/qTpiWoKuxS12cqtuYA1ssu9fRAoPQnipFgGyck3tWO63iUdxBiygq+kELFawng==} cpu: [x64] os: [linux] + libc: [glibc] - '@oven/bun-windows-aarch64@1.3.14': - resolution: {integrity: sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA==} + '@oven/bun-windows-aarch64@1.4.2': + resolution: {integrity: sha512-8EJ1ST7339WJE3poPW5nBgVW/lWf9HBz4W27ZUNhburKmcBLOByPyE6DP9fHD8FQGm5c+ilUN2hX1mrW0jxq9Q==} cpu: [arm64] os: [win32] - '@oven/bun-windows-x64-baseline@1.3.14': - resolution: {integrity: sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw==} + '@oven/bun-windows-x64@1.4.2': + resolution: {integrity: sha512-+bN6OuVld/9diT/RLSXSW7JE6CvNE3gL9XsAEjULi1nUsXd6DNO6GuA9jNdNb3r8PdJFnYHr5aypNV1Oj3Rd9g==} cpu: [x64] os: [win32] - '@oven/bun-windows-x64@1.3.14': - resolution: {integrity: sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg==} - cpu: [x64] - os: [win32] - - '@oxc-project/types@0.138.0': - resolution: {integrity: sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==} + '@oxc-project/types@0.151.0': + resolution: {integrity: sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==} '@oxfmt/binding-android-arm-eabi@0.35.0': resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} @@ -959,100 +1167,101 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@quansync/fs@1.0.0': - resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} + '@quansync/fs@1.1.0': + resolution: {integrity: sha512-qAPG/t3HqML1TlN7sY/pTbEjzFVAKsMjNNMGheyDosro+kT4iw2KCUoHcVdmliwWjorm4elZbgNQyU2eD97eDg==} + + '@rolldown/binding-android-arm-eabi@1.2.10': + resolution: {integrity: sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] - '@rolldown/binding-android-arm64@1.1.4': - resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==} + '@rolldown/binding-android-arm64@1.2.10': + resolution: {integrity: sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.4': - resolution: {integrity: sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==} + '@rolldown/binding-darwin-arm64@1.2.10': + resolution: {integrity: sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.4': - resolution: {integrity: sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==} + '@rolldown/binding-darwin-x64@1.2.10': + resolution: {integrity: sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.4': - resolution: {integrity: sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==} + '@rolldown/binding-freebsd-x64@1.2.10': + resolution: {integrity: sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.4': - resolution: {integrity: sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.10': + resolution: {integrity: sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.4': - resolution: {integrity: sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==} + '@rolldown/binding-linux-arm64-gnu@1.2.10': + resolution: {integrity: sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.4': - resolution: {integrity: sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==} + '@rolldown/binding-linux-arm64-musl@1.2.10': + resolution: {integrity: sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.4': - resolution: {integrity: sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.10': + resolution: {integrity: sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.4': - resolution: {integrity: sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.10': + resolution: {integrity: sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.4': - resolution: {integrity: sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==} + '@rolldown/binding-linux-x64-gnu@1.2.10': + resolution: {integrity: sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.4': - resolution: {integrity: sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==} + '@rolldown/binding-linux-x64-musl@1.2.10': + resolution: {integrity: sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.4': - resolution: {integrity: sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==} + '@rolldown/binding-openharmony-arm64@1.2.10': + resolution: {integrity: sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.4': - resolution: {integrity: sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.4': - resolution: {integrity: sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==} + '@rolldown/binding-win32-arm64-msvc@1.2.10': + resolution: {integrity: sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.4': - resolution: {integrity: sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==} + '@rolldown/binding-win32-x64-msvc@1.2.10': + resolution: {integrity: sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1075,6 +1284,10 @@ packages: resolution: {integrity: sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw==} engines: {node: '>=18'} + '@simple-libs/child-process-utils@2.0.0': + resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==} + engines: {node: '>=22'} + '@simple-libs/stream-utils@1.2.0': resolution: {integrity: sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==} engines: {node: '>=18'} @@ -1091,9 +1304,6 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@trivago/prettier-plugin-sort-imports@6.0.2': resolution: {integrity: sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==} engines: {node: '>= 20'} @@ -1113,8 +1323,35 @@ packages: svelte: optional: true - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + '@turbo/darwin-64@2.11.3': + resolution: {integrity: sha512-D4EIKTgPpxFQL5aZNbNqy57XOiRldM+mwP1ME9ZLM4DJoV3oNK7GKyQWVusz1umjiFNmLvunNGw7Jq4RUyYL3A==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.11.3': + resolution: {integrity: sha512-WOvgWIO86LzBQmL9aRFekROing1CZMVDjmdD7Dk6R65qFAFdrlKkufojenjW1AbUAqytpG6xsXzq8UEScdSdDQ==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.11.3': + resolution: {integrity: sha512-zIgNl9KkMnsdU54JrnazELatpMJDSktKjD3kjKVliUNMl/5uhFRrqQ+Z1qaACKw/2ybL7xzlsHir9s5q7sYlQg==} + cpu: [x64] + os: [android, linux] + + '@turbo/linux-arm64@2.11.3': + resolution: {integrity: sha512-aJToWHx5JYvpPwIrAt1DuYrHWcpQGjGf0XVPRMKQBHou//JUYR6oowHeUZp9Oq7e3XDWA75E37C2GnNaBqCVnA==} + cpu: [arm64] + os: [android, linux] + + '@turbo/windows-64@2.11.3': + resolution: {integrity: sha512-hu5biiwVGdMvHB/vvaoPjtudNBn3TqcsKK2A06ap3O0wnSZwAtTXe92tKTckpecDeVs+IC+4r8s2wXpbdSPWoA==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.11.3': + resolution: {integrity: sha512-naDEb/31UmOOJOChi+LebLMKWKa1kId0yig1yTRZzDViDg1lBw1Ptu5iQYCvOfOOrx6swUUucf3zPWuPGWz7Jw==} + cpu: [arm64] + os: [win32] '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1134,14 +1371,11 @@ packages: '@types/inquirer@9.0.10': resolution: {integrity: sha512-vFW2WbXwO9eZpRT5GJGFJ/shgyMNnYozmnjakt9jCQSS1lvqX8pZEQMjJ9RdDPct/YxwciQ8+V8OYn9euIrZDA==} - '@types/jsesc@2.5.1': - resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@26.1.0': - resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==} + '@types/node@26.6.2': + resolution: {integrity: sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==} '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} @@ -1149,79 +1383,84 @@ packages: '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} - '@typescript-eslint/eslint-plugin@8.62.1': - resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} + '@typescript-eslint/eslint-plugin@8.70.1': + resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.62.1 + '@typescript-eslint/parser': ^8.70.1 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.62.1': - resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} + '@typescript-eslint/parser@8.70.1': + resolution: {integrity: sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.62.1': - resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} + '@typescript-eslint/project-service@8.70.1': + resolution: {integrity: sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.62.1': - resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} + '@typescript-eslint/scope-manager@8.70.1': + resolution: {integrity: sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.62.1': - resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} + '@typescript-eslint/tsconfig-utils@8.70.1': + resolution: {integrity: sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.62.1': - resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} + '@typescript-eslint/type-utils@8.70.1': + resolution: {integrity: sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.62.1': - resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} + '@typescript-eslint/types@8.70.1': + resolution: {integrity: sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.62.1': - resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} + '@typescript-eslint/typescript-estree@8.70.1': + resolution: {integrity: sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.62.1': - resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} + '@typescript-eslint/utils@8.70.1': + resolution: {integrity: sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.62.1': - resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} + '@typescript-eslint/visitor-keys@8.70.1': + resolution: {integrity: sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/coverage-v8@4.1.10': - resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + '@vitest/coverage-v8@5.0.1': + resolution: {integrity: sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==} peerDependencies: - '@vitest/browser': 4.1.10 - vitest: 4.1.10 + '@vitest/browser': 5.0.1 + vitest: 5.0.1 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + '@vitest/istanbul-lib-coverage@1.0.1': + resolution: {integrity: sha512-k3DJZ8LhMBK9NS4SclF1ASD3OgXEWDorbIcPTRDK0/Zae6fRvu+fJRxtFdLfHsa9Y24beCdPnoNZ4LviTNstfA==} + engines: {node: '>=22'} - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + '@vitest/istanbul-lib-report@1.0.1': + resolution: {integrity: sha512-1EOLRfsTMnyAr3+kEAsP4o9dhaDlGPpD7H5iLBBeq//YpNB1VIahkPhB+eRp9N2Dkfw8oySROjE3yf9XDeaIkQ==} + engines: {node: '>=22'} + + '@vitest/mocker@5.0.1': + resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1231,28 +1470,151 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + '@vitest/spy@5.0.1': + resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==} + + '@yuku-codegen/binding-android-arm64@0.10.2': + resolution: {integrity: sha512-Oc2KInVkPfUjEB4PeV6X0NvIoyYTzDe/WmqFrTwBqKs6YKnz4A7XwWQvJTb7M0rLz9a6WgpUUiiS5QKaCi4O/g==} + cpu: [arm64] + os: [android] + + '@yuku-codegen/binding-darwin-arm64@0.10.2': + resolution: {integrity: sha512-3H7eNPIHJndUJXZ4QUGBPq1zC6RGcUZfm7bymJes+/N7wTVWUn1bxZ9JcozYHpkWQNm9f23G8YWbaHD9aYH72A==} + cpu: [arm64] + os: [darwin] + + '@yuku-codegen/binding-darwin-x64@0.10.2': + resolution: {integrity: sha512-AJOUpR2s3LF9AWiiub/Uyc/UoXNTWcq8hK3cVI6fUSXtwh34dG21J6hRuNlO6JVJFTo4o3ScVvZOrh/YFfUEAA==} + cpu: [x64] + os: [darwin] + + '@yuku-codegen/binding-freebsd-x64@0.10.2': + resolution: {integrity: sha512-ms7DcZu87u5CiK/wMwvpKAvAmtaqKjCQIXNweMLypG6qbuiaOMQf1jpbB5+j46xBcgCovo8TQMcv0bCQLKIL9w==} + cpu: [x64] + os: [freebsd] + + '@yuku-codegen/binding-linux-arm-gnu@0.10.2': + resolution: {integrity: sha512-xJDpYAsKV5+eaiBhTYl05fvT6sst4PsCeuIFMRu9b0/WCSrNQPfCDtAQ5/MS6xNpsdujdZEPR+gmfWk3ZG5i3A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm-musl@0.10.2': + resolution: {integrity: sha512-qfTkgd7AEx61l4K9VtXWCGTxc/fmXJ4ZheDz3i+/AAJUtg4sa0bPrc0VBxPggB/rayR8Z3+JfrBItuyanBJJ9Q==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-codegen/binding-linux-arm64-gnu@0.10.2': + resolution: {integrity: sha512-4e6Mifm/4UdjtU3D8mATIrvgP+xEiH8xtQOjd0zpd72XKWT7ug3sdyhq2utkkZFP6BvYp7SgZrI+aR4VeIOHdw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-codegen/binding-linux-arm64-musl@0.10.2': + resolution: {integrity: sha512-RdsJrUfDYFVV3JOmWFUWwSu31fa1APn12xDeIKxgl/YWxrabwtxsDBNjF2561c7tmcbiewdCUvngqRs8AyGVLA==} + cpu: [arm64] + os: [linux] + libc: [musl] - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + '@yuku-codegen/binding-linux-x64-gnu@0.10.2': + resolution: {integrity: sha512-mVzWimEPPreaPyXIUvxsHoJzvO7ckZ5j0LgQFHz04zQIRwt9A9T2hA7K5/jYjJKkMxWG/U2VjhGNwVhtyU+uqg==} + cpu: [x64] + os: [linux] + libc: [glibc] - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + '@yuku-codegen/binding-linux-x64-musl@0.10.2': + resolution: {integrity: sha512-Y77fyISurmr2mvO1yeq3u+x/WJbACgPR4w+lzEVx30ViCxZzIGrZPRN1yEdZ9xUNPNsIO5thBHIdRNG+UGUG+Q==} + cpu: [x64] + os: [linux] + libc: [musl] - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + '@yuku-codegen/binding-win32-arm64@0.10.2': + resolution: {integrity: sha512-1+tGLyG0u5YYy0lev4ck7/0sd8xJ9SZde0dLut7qLDPZV5WVTV8x6OxNXUne/PGuHZyO1vK+txPQzzHOyXHGSw==} + cpu: [arm64] + os: [win32] - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + '@yuku-codegen/binding-win32-x64@0.10.2': + resolution: {integrity: sha512-8a2SExRohRbwC0MY4VpOF0/RSckrJ2ASZqAor5/RYSJJaeadR93n10gZzcKT6pY9ukiSOIZCRKVRD2tH73T0qA==} + cpu: [x64] + os: [win32] + + '@yuku-parser/binding-android-arm64@0.10.2': + resolution: {integrity: sha512-2VPBU9fRGRAQ2xPAvghnec3oou5Nrxm2Bezhf+13UMspAxQSkF/32r+ySKXSwIPA5/RivumDJDxAxc301Sgicw==} + cpu: [arm64] + os: [android] + + '@yuku-parser/binding-darwin-arm64@0.10.2': + resolution: {integrity: sha512-LD+PMZE51tCYTOss5HBkm3/AE39MvcMDBfWJx7A4yDcjfNbAQDnHZKtzSOuqrswx+TQHY0ws5xn6+fWOwtmfBA==} + cpu: [arm64] + os: [darwin] + + '@yuku-parser/binding-darwin-x64@0.10.2': + resolution: {integrity: sha512-mmZ8cND+AoIIMRERyMinlg5ApHxP23B9jH2B5wT7T+dliPa9rubLxneB/SUjFwyUjGaFnB5G7t4YvpfbO5zbkQ==} + cpu: [x64] + os: [darwin] + + '@yuku-parser/binding-freebsd-x64@0.10.2': + resolution: {integrity: sha512-gVIjaaIddbRfAhHlC8N809wQWml7mxfSVnzaLtzGXObTeFTPAg/YVnQXt1UOhPM1ah5eG/8Y0RUlNpB4GVe7eQ==} + cpu: [x64] + os: [freebsd] + + '@yuku-parser/binding-linux-arm-gnu@0.10.2': + resolution: {integrity: sha512-q/XPPQQAPdlw05aPj30ygBhekmQryGOwxVraBgApjKK8yY1kNQgqq6XCYLF1WHSee+lhxclrTO7W0/bt7dR1GA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm-musl@0.10.2': + resolution: {integrity: sha512-A+Cb0I1hFF4wilTQpWs79k1aNnj4B2tkrHx3zsuUNF9BtVY2zPZ4yeQEM/zjXrS/qJlNMZVK9NrWJjwdpnTrfg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-arm64-gnu@0.10.2': + resolution: {integrity: sha512-aGNSzqIqqphFwAdIFwVvKIyXD1Iy3CxrEJVIZAT87Ecyi4vCmmDD2v2l9h+Gd3/wSy3JZlOI792LJbKAjyy9rQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-arm64-musl@0.10.2': + resolution: {integrity: sha512-Ddl1sF0rtuCXyHGSOV6dcmdx+ETv1iD+IVFqQuOjzkJZWclxJxdBWX6ZJMRtTiGqGUTbqLEKiTXxpR/Bvqk+2A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-linux-x64-gnu@0.10.2': + resolution: {integrity: sha512-/nlcpR6IF5U+0m5L9wvAIXeQa2DT+BXuvqKDsTcOTlcc0B4dHNyqE5hTXsS4hAyimpy2ZK8A1zMNF5hTrjg2cg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@yuku-parser/binding-linux-x64-musl@0.10.2': + resolution: {integrity: sha512-GX80dxTQD/M/OryyuxJcFzFENzry3cDFPvCFTyWogNWQH3fSHfMrNfpwpl1YzMos1eV9NygK5GSDG7VArQlb4w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@yuku-parser/binding-win32-arm64@0.10.2': + resolution: {integrity: sha512-agePQBV4VHewiGU0ACSjscZ/hJd283f9JfAF19Gf1rI5+wy5tTgx4GS36i0b3xim15AQ4RCpDRosEvhLZ4zAOw==} + cpu: [arm64] + os: [win32] + + '@yuku-parser/binding-win32-x64@0.10.2': + resolution: {integrity: sha512-s8//CMpgL5+y1lvDCdyh1rwGI5+ytDJywFJRe1vnhI7n0j+caxshNucurikYLLVeQ2SYFex6aPrzgQxkabBQRA==} + cpu: [x64] + os: [win32] + + '@yuku-toolchain/types@0.10.2': + resolution: {integrity: sha512-sSeo4SSSToiS+sSD+bwn/s94EEcaLJ7tG5LCp8gFYC1G5VzxzX7fqB6m9RU1yMD8KTpu6zdU/I1NlQf8hVBJ9Q==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -1274,31 +1636,31 @@ packages: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - ansis@4.3.1: - resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + ansis@4.4.0: + resolution: {integrity: sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==} engines: {node: '>=14'} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + argue-cli@3.2.0: + resolution: {integrity: sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw==} + engines: {node: '>=22'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-kit@3.0.0: - resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==} - engines: {node: ^22.18.0 || >=24.11.0} - - ast-v8-to-istanbul@1.0.4: - resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} + ast-v8-to-istanbul@1.0.7: + resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1310,25 +1672,22 @@ packages: before-after-hook@4.0.0: resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} - birpc@4.0.0: - resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - bottleneck@2.19.5: resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} - brace-expansion@2.1.1: - resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + brace-expansion@2.1.7: + resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - bun@1.3.14: - resolution: {integrity: sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg==} + bun@1.4.2: + resolution: {integrity: sha512-TrSXo6HJfIEaczpb3kjX82I2pL47vK1QUNmHRCUdz9IzaOwa9lzOXSWwu2l18YHE3sNfGRapVLd4nNm+22vVVA==} cpu: [arm64, x64] os: [darwin, linux, android, freebsd, win32] hasBin: true @@ -1341,6 +1700,9 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1382,10 +1744,6 @@ packages: resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} engines: {node: '>=18.20'} - cli-truncate@5.2.0: - resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} - engines: {node: '>=20'} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1405,16 +1763,16 @@ packages: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} + content-type@3.1.1: + resolution: {integrity: sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA==} + engines: {node: '>=22'} - conventional-changelog-angular@9.2.1: - resolution: {integrity: sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==} + conventional-changelog-angular@9.4.0: + resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==} engines: {node: '>=22'} - conventional-changelog-conventionalcommits@10.2.1: - resolution: {integrity: sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==} + conventional-changelog-conventionalcommits@10.4.0: + resolution: {integrity: sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==} engines: {node: '>=22'} conventional-changelog-preset-loader@5.0.0: @@ -1430,8 +1788,8 @@ packages: engines: {node: '>=18'} hasBin: true - conventional-commits-parser@7.0.1: - resolution: {integrity: sha512-6VtskFpPsNkGVk/TY2RnV/MEdKfvCPBtQZN9x8jh9+k5RWBQ+tiaWn5UFCzTr0Dd88iKx7xghxbjBRp5uIzp3g==} + conventional-commits-parser@7.1.2: + resolution: {integrity: sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==} engines: {node: '>=22'} hasBin: true @@ -1440,9 +1798,6 @@ packages: engines: {node: '>=18'} hasBin: true - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} engines: {node: '>=v18'} @@ -1460,10 +1815,19 @@ packages: typescript: optional: true + cpx2@9.0.3: + resolution: {integrity: sha512-SHeJspK34eVoLWuCrBW6YdcflnZwJifgADJNRkitsuZN2bi4+iby4slavs1qANeNiiZnIWRFG4vNcHp8UHrE5g==} + engines: {node: ^22.0.0 || >=24.0.0, npm: '>=10'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + debounce@3.0.0: + resolution: {integrity: sha512-64byRbF0/AirwbuHqB3/ZpMG9/nckDa6ZA0yd6UnaQNwbbemCOwvz2sL5sjXLHhZHADyiwLm0M5qMhltUUx+TA==} + engines: {node: '>=20'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1480,8 +1844,8 @@ packages: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} engines: {node: '>=18'} define-lazy-prop@3.0.0: @@ -1502,6 +1866,11 @@ packages: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} + dotenv@18.0.3: + resolution: {integrity: sha512-/8CJTyL817qPnfbQrJgOEcrU3fDrJkRIoXBE566wHuvIXKOCHUZhPqGPbCPi9SogjdGnreF0TlvnTgK6J/8zpA==} + engines: {node: '>=12'} + hasBin: true + dts-resolver@3.0.0: resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==} engines: {node: ^22.18.0 || >=24.0.0} @@ -1517,8 +1886,8 @@ packages: emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} - empathic@2.0.1: - resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + empathic@2.1.0: + resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} engines: {node: '>=14'} env-paths@2.2.1: @@ -1532,11 +1901,11 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-module-lexer@2.3.0: - resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - es-toolkit@1.49.0: - resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + es-toolkit@1.52.0: + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -1598,8 +1967,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.6.0: - resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + eslint@10.11.0: + resolution: {integrity: sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1631,9 +2000,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - execa@9.6.1: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} @@ -1660,8 +2026,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -1679,24 +2045,25 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-index@0.1.1: + resolution: {integrity: sha512-uJ5vWrfBKMcE6y2Z8834dwEZj9mNGxYa3t3I53OwFeuZ8D9oc2E5zcsrkuhX6h4iYrjhiv0T3szQmxlAV9uxDg==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1707,63 +2074,65 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.6.0: - resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} get-stream@9.0.1: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - get-tsconfig@5.0.0-beta.5: - resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==} + get-tsconfig@5.0.0-beta.6: + resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} engines: {node: '>=20.20.0'} - git-cliff-darwin-arm64@2.13.1: - resolution: {integrity: sha512-3ebPUnUlLmrSZDHknSZQmyZV7DEJVtKse8I25Am0cENET5Py9u9Hg9k8IRXdiDtHtPDs6MYZx7BOi11WtcfqSg==} + git-cliff-darwin-arm64@2.14.2: + resolution: {integrity: sha512-Ylk39hv5flEUrXeoGvH6M3WjHBNERFH7a9DVEcGU2a8Q7a7fPtVasNk1fMuJ7CVbZCRIzUNyQV0ejRpxBdFnag==} cpu: [arm64] os: [darwin] - git-cliff-darwin-x64@2.13.1: - resolution: {integrity: sha512-KZfggGAiw1EvZH3BOUclEU4eGCP2+Z+lH/N2Ni3FH9L2M1U7FYJqqaMhqgO8azTj67betvDshH8WBUkIkSsVxA==} + git-cliff-darwin-x64@2.14.2: + resolution: {integrity: sha512-3FZNm0BGCIim/3G6uHSWAakrCaGmC47lcI+Cob9CZPMBYHKrenYk0i6B8MlObSXr2Lr7Ve1BpGbd/foQg4NOiA==} cpu: [x64] os: [darwin] - git-cliff-linux-arm64@2.13.1: - resolution: {integrity: sha512-clNRcNzdvk4GKyTt3ZhhWqcrjVRghcUcGNSV/Y87YJf3Mc/zl9ajUAhnXPOBSX/KWBr2od5SmkCt0qGYagY07g==} + git-cliff-linux-arm64@2.14.2: + resolution: {integrity: sha512-3oMDpoyfsy2angD9ulcCBtyWCkzZqN3spmmtb0jic8OKAu1p+CALS8oep7J8AdgkDojuLo6bdHiSZIM9PeTxmQ==} cpu: [arm64] os: [linux] - git-cliff-linux-x64@2.13.1: - resolution: {integrity: sha512-gZcIQhIQ1lDUMD8UieUSEZAYoyZN7nPd8O3VQoj1Ddhqll4UAS1Zxms1SU3U8pb0UTdc2m3ia/wtOnyhvbjdjQ==} + git-cliff-linux-x64@2.14.2: + resolution: {integrity: sha512-4EaBYwBiCupECZ2rzC6CelNSGYvkvtJeja4wXLDGtfKnRhCN/WNoqbM+hvrtm32uf5qovfoNoDA906F8itw+oQ==} cpu: [x64] os: [linux] - git-cliff-windows-arm64@2.13.1: - resolution: {integrity: sha512-+XubuQv68DuDwF0u6Af5d889MEf/RD48VBQS7ZmPi4sUSCXAZqRm1/ApCsStLqxMDCf1+7s05B/kNbm9wjV80A==} + git-cliff-windows-arm64@2.14.2: + resolution: {integrity: sha512-0oc0xD/6A+BvInz8imBJulXVOlR9M0nL+d3ibL0lwJvxUVTMQzvoCnsn9OxpsdzXrEisnTvaf+GExoKVJkUcDw==} cpu: [arm64] os: [win32] - git-cliff-windows-x64@2.13.1: - resolution: {integrity: sha512-Bi8ehp1VMkomY7M/356PgovKQ8CBRiuOOkq+aWC2evQuMFfXWjG0GBlPED9QkZoUJ4iQ5ygB5DYdK3BjhaOyPw==} + git-cliff-windows-x64@2.14.2: + resolution: {integrity: sha512-RFmCMiwGMU8O+rF//wxT7L10ssoDtzI+QFxlbjpA8E1BAEafkmLTQkKalT88ipEweD46P+Sfc20v8RXuE7V/UA==} cpu: [x64] os: [win32] - git-cliff@2.13.1: - resolution: {integrity: sha512-2BzXwrom+SMHeNA5Ut+MtzqXejg0wbVwmzj3k7e9w62UQoyCDrM9UIcvtl6hnT3jocEQ1zLRQBaXXx97Gmnk7A==} + git-cliff@2.14.2: + resolution: {integrity: sha512-hlXIq4k2Tdj9C59WsD7FsLE0xRGFDozTUuoeqg9toMyVohaVxXiSiLnhM/IQyCmhOh5M4zuNckr2o8qMeuffbg==} engines: {node: ^18.19 || >=20.6} hasBin: true - git-raw-commits@5.0.1: - resolution: {integrity: sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ==} - engines: {node: '>=18'} - deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead. - hasBin: true - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob2base@0.0.12: + resolution: {integrity: sha512-ZyqlgowMbfj2NPjxaZZ/EtsXlOch28FRXgMd64vqZWk1bT9+wvSRLYD1om9M7QfQru51zJPAT17qXm4/zd+9QA==} + engines: {node: '>= 0.10'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + global-directory@5.0.0: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} @@ -1772,19 +2141,22 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - has-flag@5.0.1: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} - hookable@6.1.1: - resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hookable@6.1.2: + resolution: {integrity: sha512-+abwxtiEA52GCVIsQqut3S/uKTbUwYIp4Pe/vv+6py5XiXBCqMZHg6pA6Y5qhgLSEys0/cYuPbO0z1QFj5ZCmg==} - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} human-signals@8.0.1: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} @@ -1803,16 +2175,16 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.10: + resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==} engines: {node: '>= 4'} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-without-cache@0.4.0: - resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==} + import-without-cache@0.4.1: + resolution: {integrity: sha512-vXoV9PjKHEednCUu01e98TkImxy67e3BJXbTIOmIq4Hyzw3IAwYRcuPkfeTzJzwyyts4kjuA73x+pWJLc4f86A==} engines: {node: ^22.18.0 || >=24.0.0} imurmurhash@0.1.4: @@ -1839,10 +2211,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@5.1.0: - resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} - engines: {node: '>=18'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1883,18 +2251,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - javascript-natural-sort@0.7.1: resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} @@ -1908,8 +2264,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsesc@3.1.0: @@ -1917,9 +2273,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -1932,108 +2285,104 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json-with-bigint@3.5.12: + resolution: {integrity: sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==} jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.13.8: - resolution: {integrity: sha512-80xal1m93rADejw2pMp2MSzFhHCPLEspjHxnH2UtqI+DgAmElsbmLMiqk9niwH9NWAfjsRtaJI+qBrOEmRx9nQ==} + libphonenumber-js@1.13.13: + resolution: {integrity: sha512-hV6SmWnyQ8U1QXhhqbqYBHIStNnscG7A5XbGodgdHhf/8N5PwhYpJA5U/yHDjYwPYTd8uNGXqq416XctuvK2PA==} - lightningcss-android-arm64@1.32.0: - resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.32.0: - resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.32.0: - resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.32.0: - resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: - resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [glibc] - lightningcss-linux-arm64-musl@1.32.0: - resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] libc: [musl] - lightningcss-linux-x64-gnu@1.32.0: - resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [glibc] - lightningcss-linux-x64-musl@1.32.0: - resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] libc: [musl] - lightningcss-win32-arm64-msvc@1.32.0: - resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.32.0: - resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.32.0: - resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lint-staged@17.0.8: - resolution: {integrity: sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==} + lint-staged@17.5.1: + resolution: {integrity: sha512-7EDuco1xnBMeVpvAbeMq1U5KXwJGLmY8q6l+Ye78r36C4mPc+Vg1Z2SK8gHyRTyvPro90A0q5/FAZ+Au23d6QQ==} engines: {node: '>=22.22.1'} hasBin: true - listr2@10.2.2: - resolution: {integrity: sha512-JtNtbZj8q5BnDMR7trpwvwk3RIrANtIVzEUm8w7amp6xelLgyuq+4WZoTH913XaQAoH/cNdYhaNzBPA2U3xbDw==} - engines: {node: '>=22.13.0'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2045,28 +2394,23 @@ packages: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} - log-update@6.1.0: - resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} - engines: {node: '>=18'} + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@1.0.0: + resolution: {integrity: sha512-CGvjzMN08iv6w1mm4/x3Gh1hLb4VnyRUA15FFpl6CsCIGGoe36k7kY5KNz9QDbSBN5I/fWHM6ZlIkUTa5xdUEA==} - magicast@0.5.3: - resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + magic-string@1.4.2: + resolution: {integrity: sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==} - make-dir@4.0.0: - resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} - engines: {node: '>=10'} + magicast@0.5.5: + resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} - meow@14.1.0: - resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} - engines: {node: '>=20'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2075,14 +2419,17 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -2098,8 +2445,8 @@ packages: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2114,29 +2461,29 @@ packages: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} - obug@2.1.3: - resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} engines: {node: '>=12.20.0'} - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} + engines: {node: '>=12.20.0'} + + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + open@11.0.4: + resolution: {integrity: sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==} engines: {node: '>=20'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@9.4.0: - resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==} - engines: {node: '>=20'} - ora@9.4.1: resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} engines: {node: '>=20'} @@ -2154,6 +2501,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-map@7.0.8: + resolution: {integrity: sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw==} + engines: {node: '>=18'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2184,8 +2535,9 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2194,26 +2546,30 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - picomatch@4.0.5: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + plur@5.1.0: resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - postcss@8.5.16: - resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2222,27 +2578,31 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.9.4: - resolution: {integrity: sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==} + prettier@3.9.9: + resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} engines: {node: '>=14'} hasBin: true - pretty-ms@9.3.0: - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + pretty-ms@9.3.1: + resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==} engines: {node: '>=18'} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} - rc9@3.0.1: - resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + rc9@3.1.0: + resolution: {integrity: sha512-ufjkNVzbRHKcCOmTahZkmVsyc3W+MSk3jY03m+a7tGHkIsdVMG9l10/3HvFbWkkKzY5VFp3pkRsIo/UYgmFL7Q==} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} engines: {node: '>= 20.19.0'} reflect-metadata@0.2.2: @@ -2267,30 +2627,30 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - - rolldown-plugin-dts@0.26.0: - resolution: {integrity: sha512-e+kEPtUiDES0htk5iqkSeF4EzAV7R+vugGB44iPDuw1Kw9E+WyL1VG7PaV0IIjGHLiacztMBcMTyrr8ON9CT1Q==} - engines: {node: ^22.18.0 || >=24.11.0} + rolldown-plugin-dts@0.28.6: + resolution: {integrity: sha512-qKrFtBfRfR2hP233m7Ic9zf3wv6MSYdZghWKMdEO6dRYZGlNfINJAa1P5ZVvyHh3mrcd7IJQvIY8L9vnm+v5rQ==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} peerDependencies: - '@ts-macro/tsc': ^0.3.6 - '@typescript/native-preview': '>=7.0.0-dev.20260325.1' - rolldown: ^1.0.0 - typescript: ^5.0.0 || ^6.0.0 + '@typescript/native-preview': '*' + '@volar/typescript': ~2.4.0 + '@vue/language-core': ~3.2.0 || ~3.3.0 + rolldown: ^1.2.0 + typescript: ^5.0.0 || ^6.0.0 || ~7.0.0 vue-tsc: ~3.2.0 || ~3.3.0 peerDependenciesMeta: - '@ts-macro/tsc': - optional: true '@typescript/native-preview': optional: true + '@volar/typescript': + optional: true + '@vue/language-core': + optional: true typescript: optional: true vue-tsc: optional: true - rolldown@1.1.4: - resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} + rolldown@1.2.10: + resolution: {integrity: sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2317,6 +2677,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2328,16 +2692,8 @@ packages: resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==} engines: {node: '>=8'} - slice-ansi@7.1.2: - resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} - engines: {node: '>=18'} - - slice-ansi@8.0.0: - resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} - engines: {node: '>=20'} - - smol-toml@1.7.0: - resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -2351,8 +2707,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@4.1.0: - resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} stdin-discarder@0.3.2: resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} @@ -2366,8 +2722,8 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.2.1: - resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} engines: {node: '>=20'} strip-ansi@7.2.0: @@ -2378,14 +2734,13 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + subarg@1.0.0: + resolution: {integrity: sha512-RIrIdRY0X1xojthNcVtgT9sjpOGagEUKpZdgBUi054OEPFo282yg+zE+t1Rj3+RqKq2xStL7uUHhY+AjbC4BXg==} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - supports-hyperlinks@4.5.0: resolution: {integrity: sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==} engines: {node: '>=20'} @@ -2394,15 +2749,20 @@ packages: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} engines: {node: ^14.18.0 || >=16.0.0} - tar@7.5.19: - resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} engines: {node: '>=18'} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} engines: {node: '>=18'} tinyglobby@0.2.17: @@ -2413,8 +2773,8 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.1.0: - resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: @@ -2431,19 +2791,19 @@ packages: peerDependencies: typescript: '>=4.8.4' - tsdown@0.22.3: - resolution: {integrity: sha512-louqbfA8Qf//B9jTTL0FPtXTNpjCWv1VPkbcmQMph2pTpzs+LnB1tbe4tDDRVpo2BjF5SgUXaTZe45SxB8pWHg==} - engines: {node: ^22.18.0 || >=24.11.0} + tsdown@0.23.0: + resolution: {integrity: sha512-BaT+ep1xnj5hdyICLd5r5SYYE+TXnI1ATePLykv9vcKpHpFTWUWRH+x1AF/E2qcu3YB6+vI8IdyxIIIffEqw5Q==} + engines: {node: ^22.18.0 || ^24.11.0 || >=26.0.0} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.22.3 - '@tsdown/exe': 0.22.3 + '@tsdown/css': 0.23.0 + '@tsdown/exe': 0.23.0 '@vitejs/devtools': '*' publint: ^0.3.8 tsx: '*' - typescript: ^5.0.0 || ^6.0.0 - unplugin-unused: ^0.5.0 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 + unplugin-unused: '>=0.5.0' unrun: '*' peerDependenciesMeta: '@arethetypeswrong/core': @@ -2472,12 +2832,16 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + turbo@2.11.3: + resolution: {integrity: sha512-Pfg2dWNOUPwJYbR+eYAcjoBbo4Kmoc8uQb80hzKrBgNEQtxvwf2ucXG7C28MyLVhV/zJSKvChrR+2CP/BONpoA==} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.62.1: - resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} + typescript-eslint@8.70.1: + resolution: {integrity: sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2491,11 +2855,11 @@ packages: unconfig-core@7.5.0: resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==} - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} - undici@6.27.0: - resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} + undici@6.28.1: + resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} engines: {node: '>=18.17'} unicode-emoji-modifier-base@1.0.0: @@ -2509,6 +2873,16 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + unrun@0.3.1: + resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==} + engines: {node: ^22.13.0 || >=24.0.0} + hasBin: true + peerDependencies: + synckit: ^0.11.11 + peerDependenciesMeta: + synckit: + optional: true + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2516,13 +2890,17 @@ packages: resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} engines: {node: '>= 0.10'} - vite@8.1.2: - resolution: {integrity: sha512-6YYPbRXTxx6bRXmOn7XdnQAy5DQNHhDgtjhDHI13oe4pY93kkcdGJWxpGwOm++/Wh0QpQhDrpIoVMrmrsI5AGQ==} + verkit@0.4.1: + resolution: {integrity: sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==} + engines: {node: '>=18.12.0'} + + vite@8.3.0: + resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.7.1 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -2559,23 +2937,23 @@ packages: yaml: optional: true - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.1: + resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.1 + '@vitest/browser-preview': 5.0.1 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.1 + '@vitest/coverage-v8': 5.0.1 + '@vitest/ui': 5.0.1 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -2614,16 +2992,12 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@10.0.0: - resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} - engines: {node: '>=20'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + wsl-utils@1.0.0: + resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==} engines: {node: '>=20'} y18n@5.0.8: @@ -2634,8 +3008,8 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + yaml@2.9.1: + resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==} engines: {node: '>= 14.6'} hasBin: true @@ -2643,18 +3017,30 @@ packages: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@18.0.0: - resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + yargs@18.2.0: + resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} + yuku-ast@0.10.2: + resolution: {integrity: sha512-UnG9mA6giglCvSErft2/40TVFX750Sj1xgwddPLpG7J7rlr/P1wPADg9G2RK+d/1tNLtRVoLdMMOMVBB9561TQ==} + + yuku-codegen@0.10.2: + resolution: {integrity: sha512-hentl2dtrF6cPjiAirtkfxfFZBA6Hdm61UqRJ7cA0qrlDNMk9PZGOjw5w1mx3E0hu/6pHcJF4dJW+D0SXHPZOA==} + + yuku-parser@0.10.2: + resolution: {integrity: sha512-CgaU0/PPjCAIEZ3WQroosOxTY3eeKldAN3h+vk8pMNz4+jl1CZzBR4pW+K/rRPF112VooCL5FdjJoiMNjDrL2A==} + + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + snapshots: '@actions/core@3.0.1': @@ -2669,51 +3055,51 @@ snapshots: '@actions/github@9.1.1': dependencies: '@actions/http-client': 3.0.2 - '@octokit/core': 7.0.6 - '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.6) - '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.6) - '@octokit/request': 10.0.11 - '@octokit/request-error': 7.1.0 - undici: 6.27.0 + '@octokit/core': 7.0.8 + '@octokit/plugin-paginate-rest': 14.0.0(@octokit/core@7.0.8) + '@octokit/plugin-rest-endpoint-methods': 17.0.0(@octokit/core@7.0.8) + '@octokit/request': 10.0.16 + '@octokit/request-error': 7.1.2 + undici: 6.28.1 '@actions/http-client@3.0.2': dependencies: tunnel: 0.0.6 - undici: 6.27.0 + undici: 6.28.1 '@actions/http-client@4.0.1': dependencies: tunnel: 0.0.6 - undici: 6.27.0 + undici: 6.28.1 '@actions/io@3.0.2': {} - '@angular-devkit/core@22.0.5(chokidar@5.0.0)': + '@angular-devkit/core@22.1.8(chokidar@5.0.0)': dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) jsonc-parser: 3.3.1 - picomatch: 4.0.4 + picomatch: 4.0.5 rxjs: 7.8.2 source-map: 0.7.6 optionalDependencies: chokidar: 5.0.0 - '@angular-devkit/schematics-cli@22.0.5(@types/node@26.1.0)(chokidar@5.0.0)': + '@angular-devkit/schematics-cli@22.1.8(@types/node@26.6.2)(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 22.0.5(chokidar@5.0.0) - '@angular-devkit/schematics': 22.0.5(chokidar@5.0.0) - '@inquirer/prompts': 8.4.2(@types/node@26.1.0) + '@angular-devkit/core': 22.1.8(chokidar@5.0.0) + '@angular-devkit/schematics': 22.1.8(chokidar@5.0.0) + '@inquirer/prompts': 8.5.2(@types/node@26.6.2) transitivePeerDependencies: - '@types/node' - chokidar - '@angular-devkit/schematics@22.0.5(chokidar@5.0.0)': + '@angular-devkit/schematics@22.1.8(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 22.0.5(chokidar@5.0.0) + '@angular-devkit/core': 22.1.8(chokidar@5.0.0) jsonc-parser: 3.3.1 - magic-string: 0.30.21 - ora: 9.4.0 + magic-string: 1.0.0 + ora: 9.4.1 rxjs: 7.8.2 transitivePeerDependencies: - chokidar @@ -2724,130 +3110,136 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/generator@7.29.7': + '@babel/generator@7.29.8': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/generator@8.0.0': - dependencies: - '@babel/parser': 8.0.0 - '@babel/types': 8.0.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@types/jsesc': 2.5.1 - jsesc: 3.1.0 - '@babel/helper-globals@7.29.7': {} '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-string-parser@8.0.0': {} - '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-identifier@8.0.2': {} - - '@babel/parser@7.29.7': + '@babel/parser@7.29.9': dependencies: - '@babel/types': 7.29.7 - - '@babel/parser@8.0.0': - dependencies: - '@babel/types': 8.0.0 + '@babel/types': 7.29.8 '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.8(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.9 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3 + '@babel/types': 7.29.8 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@8.0.0': + '@bcoe/v8-coverage@1.0.2': {} + + '@cacheable/memory@2.2.0': dependencies: - '@babel/helper-string-parser': 8.0.0 - '@babel/helper-validator-identifier': 8.0.2 + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 - '@bcoe/v8-coverage@1.0.2': {} + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 - '@commitlint/cli@21.2.0(@types/node@26.1.0)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)': + '@commitlint/cli@21.2.3(@types/node@26.6.2)(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)(typescript@6.0.3)': dependencies: - '@commitlint/config-conventional': 21.2.0 - '@commitlint/format': 21.2.0 - '@commitlint/lint': 21.2.0 - '@commitlint/load': 21.2.0(@types/node@26.1.0)(typescript@6.0.3) - '@commitlint/read': 21.2.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) - '@commitlint/types': 21.2.0 - tinyexec: 1.2.4 - yargs: 18.0.0 + '@commitlint/config-conventional': 21.2.3 + '@commitlint/format': 21.2.3 + '@commitlint/lint': 21.2.3 + '@commitlint/load': 21.2.3(@types/node@26.6.2)(typescript@6.0.3) + '@commitlint/read': 21.2.3(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) + '@commitlint/types': 21.2.3 + tinyexec: 1.3.1 + yargs: 18.2.0 transitivePeerDependencies: - '@types/node' - conventional-commits-filter - conventional-commits-parser - typescript - '@commitlint/config-conventional@21.2.0': + '@commitlint/cli@21.2.3(@types/node@26.6.2)(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': dependencies: - '@commitlint/types': 21.2.0 - conventional-changelog-conventionalcommits: 10.2.1 + '@commitlint/config-conventional': 21.2.3 + '@commitlint/format': 21.2.3 + '@commitlint/lint': 21.2.3 + '@commitlint/load': 21.2.3(@types/node@26.6.2)(typescript@6.0.3) + '@commitlint/read': 21.2.3(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2) + '@commitlint/types': 21.2.3 + tinyexec: 1.3.1 + yargs: 18.2.0 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript - '@commitlint/config-validator@21.2.0': + '@commitlint/config-conventional@21.2.3': dependencies: - '@commitlint/types': 21.2.0 + '@commitlint/types': 21.2.3 + conventional-changelog-conventionalcommits: 10.4.0 + + '@commitlint/config-validator@21.2.3': + dependencies: + '@commitlint/types': 21.2.3 ajv: 8.20.0 - '@commitlint/ensure@21.2.0': + '@commitlint/ensure@21.2.3': dependencies: - '@commitlint/types': 21.2.0 - es-toolkit: 1.49.0 + '@commitlint/types': 21.2.3 + es-toolkit: 1.52.0 '@commitlint/execute-rule@21.0.1': {} - '@commitlint/format@21.2.0': + '@commitlint/format@21.2.3': dependencies: - '@commitlint/types': 21.2.0 + '@commitlint/types': 21.2.3 picocolors: 1.1.1 - '@commitlint/is-ignored@21.2.0': + '@commitlint/is-ignored@21.2.3': dependencies: - '@commitlint/types': 21.2.0 + '@commitlint/types': 21.2.3 semver: 7.8.5 - '@commitlint/lint@21.2.0': + '@commitlint/lint@21.2.3': dependencies: - '@commitlint/is-ignored': 21.2.0 - '@commitlint/parse': 21.2.0 - '@commitlint/rules': 21.2.0 - '@commitlint/types': 21.2.0 + '@commitlint/is-ignored': 21.2.3 + '@commitlint/parse': 21.2.3 + '@commitlint/rules': 21.2.3 + '@commitlint/types': 21.2.3 - '@commitlint/load@21.2.0(@types/node@26.1.0)(typescript@6.0.3)': + '@commitlint/load@21.2.3(@types/node@26.6.2)(typescript@6.0.3)': dependencies: - '@commitlint/config-validator': 21.2.0 + '@commitlint/config-validator': 21.2.3 '@commitlint/execute-rule': 21.0.1 - '@commitlint/resolve-extends': 21.2.0 - '@commitlint/types': 21.2.0 + '@commitlint/resolve-extends': 21.2.3 + '@commitlint/types': 21.2.3 cosmiconfig: 9.0.2(typescript@6.0.3) - cosmiconfig-typescript-loader: 6.3.0(@types/node@26.1.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) - es-toolkit: 1.49.0 + cosmiconfig-typescript-loader: 6.3.0(@types/node@26.6.2)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) + es-toolkit: 1.52.0 is-plain-obj: 4.1.0 picocolors: 1.1.1 transitivePeerDependencies: @@ -2856,36 +3248,46 @@ snapshots: '@commitlint/message@21.2.0': {} - '@commitlint/parse@21.2.0': + '@commitlint/parse@21.2.3': + dependencies: + '@commitlint/types': 21.2.3 + conventional-changelog-angular: 9.4.0 + conventional-commits-parser: 7.1.2 + + '@commitlint/read@21.2.3(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)': dependencies: - '@commitlint/types': 21.2.0 - conventional-changelog-angular: 9.2.1 - conventional-commits-parser: 7.0.1 + '@commitlint/top-level': 21.2.0 + '@commitlint/types': 21.2.3 + '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) + tinyexec: 1.3.1 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser - '@commitlint/read@21.2.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)': + '@commitlint/read@21.2.3(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)': dependencies: '@commitlint/top-level': 21.2.0 - '@commitlint/types': 21.2.0 - git-raw-commits: 5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) - tinyexec: 1.2.4 + '@commitlint/types': 21.2.3 + '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2) + tinyexec: 1.3.1 transitivePeerDependencies: - conventional-commits-filter - conventional-commits-parser - '@commitlint/resolve-extends@21.2.0': + '@commitlint/resolve-extends@21.2.3': dependencies: - '@commitlint/config-validator': 21.2.0 - '@commitlint/types': 21.2.0 - es-toolkit: 1.49.0 + '@commitlint/config-validator': 21.2.3 + '@commitlint/types': 21.2.3 + es-toolkit: 1.52.0 global-directory: 5.0.0 resolve-from: 5.0.0 - '@commitlint/rules@21.2.0': + '@commitlint/rules@21.2.3': dependencies: - '@commitlint/ensure': 21.2.0 + '@commitlint/ensure': 21.2.3 '@commitlint/message': 21.2.0 '@commitlint/to-lines': 21.0.1 - '@commitlint/types': 21.2.0 + '@commitlint/types': 21.2.3 '@commitlint/to-lines@21.0.1': {} @@ -2893,21 +3295,39 @@ snapshots: dependencies: escalade: 3.2.0 - '@commitlint/types@21.2.0': + '@commitlint/types@21.2.3': dependencies: - conventional-commits-parser: 7.0.1 + conventional-commits-parser: 7.1.2 picocolors: 1.1.1 '@conventional-changelog/git-client@2.7.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)': dependencies: - '@simple-libs/child-process-utils': 1.0.2 - '@simple-libs/stream-utils': 1.2.0 + '@simple-libs/child-process-utils': 1.0.2 + '@simple-libs/stream-utils': 1.2.0 + semver: 7.8.5 + optionalDependencies: + conventional-commits-filter: 5.0.0 + conventional-commits-parser: 6.4.0 + + '@conventional-changelog/git-client@3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0)': + dependencies: + '@simple-libs/child-process-utils': 2.0.0 + '@simple-libs/stream-utils': 2.0.0 semver: 7.8.5 optionalDependencies: conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.4.0 - '@conventional-changelog/template@1.2.1': {} + '@conventional-changelog/git-client@3.1.2(conventional-commits-filter@5.0.0)(conventional-commits-parser@7.1.2)': + dependencies: + '@simple-libs/child-process-utils': 2.0.0 + '@simple-libs/stream-utils': 2.0.0 + semver: 7.8.5 + optionalDependencies: + conventional-commits-filter: 5.0.0 + conventional-commits-parser: 7.1.2 + + '@conventional-changelog/template@1.4.0': {} '@dprint/formatter@0.5.1': {} @@ -2915,38 +3335,22 @@ snapshots: '@dprint/toml@0.7.0': {} - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))': dependencies: - eslint: 10.6.0(jiti@2.6.1) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@10.2.2)': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.6 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.6.0': + '@eslint/config-helpers@0.7.0': dependencies: '@eslint/core': 1.2.1 @@ -2954,11 +3358,11 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@9.39.4': {} + '@eslint/js@9.39.5': {} '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.7.2': + '@eslint/plugin-kit@0.7.3': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 @@ -2967,18 +3371,18 @@ snapshots: dependencies: '@favware/colorette-spinner': 1.0.1 '@octokit/auth-token': 6.0.0 - '@octokit/core': 7.0.6 - '@octokit/plugin-retry': 8.1.0(@octokit/core@7.0.6) + '@octokit/core': 7.0.8 + '@octokit/plugin-retry': 8.1.1(@octokit/core@7.0.8) '@sapphire/result': 2.8.0 '@sapphire/utilities': 3.18.2 colorette: 2.0.20 commander: 14.0.3 conventional-recommended-bump: 11.2.0 execa: 9.6.1 - git-cliff: 2.13.1 - js-yaml: 4.3.0 + git-cliff: 2.14.2 + js-yaml: 4.3.2 semver: 7.8.5 - smol-toml: 1.7.0 + smol-toml: 1.9.0 '@favware/colorette-spinner@1.0.1': dependencies: @@ -3000,158 +3404,165 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@2.0.7': {} + '@inquirer/ansi@2.0.8': {} - '@inquirer/checkbox@5.2.1(@types/node@26.1.0)': + '@inquirer/checkbox@5.2.5(@types/node@26.6.2)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/confirm@6.1.1(@types/node@26.1.0)': + '@inquirer/confirm@6.3.2(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/core@11.2.1(@types/node@26.1.0)': + '@inquirer/core@12.0.3(@types/node@26.6.2)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/ansi': 2.0.8 + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.6.2) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/editor@5.2.2(@types/node@26.1.0)': + '@inquirer/editor@5.3.3(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/external-editor': 3.0.3(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/external-editor': 3.0.5(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/expand@5.1.1(@types/node@26.1.0)': + '@inquirer/expand@5.1.5(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/external-editor@3.0.3(@types/node@26.1.0)': + '@inquirer/external-editor@3.0.5(@types/node@26.6.2)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/figures@2.0.7': {} + '@inquirer/figures@2.0.9': {} - '@inquirer/input@5.1.2(@types/node@26.1.0)': + '@inquirer/input@5.1.6(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/number@4.1.1(@types/node@26.1.0)': + '@inquirer/number@4.2.3(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/password@5.1.1(@types/node@26.1.0)': + '@inquirer/password@5.2.2(@types/node@26.6.2)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 - - '@inquirer/prompts@8.4.2(@types/node@26.1.0)': - dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@26.1.0) - '@inquirer/confirm': 6.1.1(@types/node@26.1.0) - '@inquirer/editor': 5.2.2(@types/node@26.1.0) - '@inquirer/expand': 5.1.1(@types/node@26.1.0) - '@inquirer/input': 5.1.2(@types/node@26.1.0) - '@inquirer/number': 4.1.1(@types/node@26.1.0) - '@inquirer/password': 5.1.1(@types/node@26.1.0) - '@inquirer/rawlist': 5.3.1(@types/node@26.1.0) - '@inquirer/search': 4.2.1(@types/node@26.1.0) - '@inquirer/select': 5.2.1(@types/node@26.1.0) + '@types/node': 26.6.2 + + '@inquirer/prompts@8.5.2(@types/node@26.6.2)': + dependencies: + '@inquirer/checkbox': 5.2.5(@types/node@26.6.2) + '@inquirer/confirm': 6.3.2(@types/node@26.6.2) + '@inquirer/editor': 5.3.3(@types/node@26.6.2) + '@inquirer/expand': 5.1.5(@types/node@26.6.2) + '@inquirer/input': 5.1.6(@types/node@26.6.2) + '@inquirer/number': 4.2.3(@types/node@26.6.2) + '@inquirer/password': 5.2.2(@types/node@26.6.2) + '@inquirer/rawlist': 5.3.5(@types/node@26.6.2) + '@inquirer/search': 4.3.3(@types/node@26.6.2) + '@inquirer/select': 5.2.5(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 - - '@inquirer/prompts@8.5.2(@types/node@26.1.0)': - dependencies: - '@inquirer/checkbox': 5.2.1(@types/node@26.1.0) - '@inquirer/confirm': 6.1.1(@types/node@26.1.0) - '@inquirer/editor': 5.2.2(@types/node@26.1.0) - '@inquirer/expand': 5.1.1(@types/node@26.1.0) - '@inquirer/input': 5.1.2(@types/node@26.1.0) - '@inquirer/number': 4.1.1(@types/node@26.1.0) - '@inquirer/password': 5.1.1(@types/node@26.1.0) - '@inquirer/rawlist': 5.3.1(@types/node@26.1.0) - '@inquirer/search': 4.2.1(@types/node@26.1.0) - '@inquirer/select': 5.2.1(@types/node@26.1.0) + '@types/node': 26.6.2 + + '@inquirer/prompts@8.7.2(@types/node@26.6.2)': + dependencies: + '@inquirer/checkbox': 5.2.5(@types/node@26.6.2) + '@inquirer/confirm': 6.3.2(@types/node@26.6.2) + '@inquirer/editor': 5.3.3(@types/node@26.6.2) + '@inquirer/expand': 5.1.5(@types/node@26.6.2) + '@inquirer/input': 5.1.6(@types/node@26.6.2) + '@inquirer/number': 4.2.3(@types/node@26.6.2) + '@inquirer/password': 5.2.2(@types/node@26.6.2) + '@inquirer/rawlist': 5.3.5(@types/node@26.6.2) + '@inquirer/search': 4.3.3(@types/node@26.6.2) + '@inquirer/select': 5.2.5(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/rawlist@5.3.1(@types/node@26.1.0)': + '@inquirer/rawlist@5.3.5(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/search@4.2.1(@types/node@26.1.0)': + '@inquirer/search@4.3.3(@types/node@26.6.2)': dependencies: - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/select@5.2.1(@types/node@26.1.0)': + '@inquirer/select@5.2.5(@types/node@26.6.2)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 11.2.1(@types/node@26.1.0) - '@inquirer/figures': 2.0.7 - '@inquirer/type': 4.0.7(@types/node@26.1.0) + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.3(@types/node@26.6.2) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.6.2) optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 - '@inquirer/type@4.0.7(@types/node@26.1.0)': + '@inquirer/type@4.1.1(@types/node@26.6.2)': optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.3 - optional: true '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} '@nanoforge-dev/actions@2.1.5': dependencies: @@ -3162,45 +3573,37 @@ snapshots: '@nanoforge-dev/editor@0.1.4': dependencies: - bun: 1.3.14 + bun: 1.4.2 class-transformer: 0.5.1 class-validator: 0.15.1 dotenv: 17.4.2 micromatch: 4.0.8 - tar: 7.5.19 + tar: 7.5.22 typescript: 6.0.3 - optional: true - '@nanoforge-dev/loader-client@1.4.0': + '@nanoforge-dev/loader-client@1.5.1': dependencies: - '@nanoforge-dev/loader-website': 1.4.0 - bun: 1.3.14 + '@nanoforge-dev/loader-website': 1.5.1 + bun: 1.4.2 commander: 15.0.0 - '@nanoforge-dev/loader-server@1.4.0': + '@nanoforge-dev/loader-server@1.5.1': dependencies: + bun: 1.4.2 commander: 15.0.0 - '@nanoforge-dev/loader-website@1.4.0': {} - - '@nanoforge-dev/schematics@2.2.1(chokidar@5.0.0)': - dependencies: - '@angular-devkit/core': 22.0.5(chokidar@5.0.0) - '@angular-devkit/schematics': 22.0.5(chokidar@5.0.0) - rxjs: 7.8.2 - transitivePeerDependencies: - - chokidar + '@nanoforge-dev/loader-website@1.5.1': {} - '@nanoforge-dev/utils-eslint-config@1.0.2(@types/eslint@9.6.1)(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4)(typescript@6.0.3)': + '@nanoforge-dev/utils-eslint-config@1.0.2(@types/eslint@9.6.1)(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.9)(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@eslint/js': 9.39.4 + '@eslint/js': 9.39.5 '@favware/cliff-jumper': 6.1.0 - eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)) eslint-formatter-pretty: 7.1.0 - eslint-plugin-format: 1.5.0(eslint@10.6.0(jiti@2.6.1)) - eslint-plugin-prettier: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.6.1)))(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4) + eslint-plugin-format: 1.5.0(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)) + eslint-plugin-prettier: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)))(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.9) globals: 16.5.0 - typescript-eslint: 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) + typescript-eslint: 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) transitivePeerDependencies: - '@types/eslint' - eslint @@ -3210,121 +3613,114 @@ snapshots: '@nanoforge-dev/utils-prettier-config@1.0.2': {} - '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - '@octokit/auth-token@6.0.0': {} - '@octokit/core@7.0.6': + '@octokit/core@7.0.8': dependencies: '@octokit/auth-token': 6.0.0 - '@octokit/graphql': 9.0.3 - '@octokit/request': 10.0.11 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 + '@octokit/graphql': 9.0.5 + '@octokit/request': 10.0.16 + '@octokit/request-error': 7.1.2 + '@octokit/types': 18.0.0 before-after-hook: 4.0.0 universal-user-agent: 7.0.3 - '@octokit/endpoint@11.0.3': + '@octokit/endpoint@11.0.5': dependencies: - '@octokit/types': 16.0.0 + '@octokit/types': 18.0.0 universal-user-agent: 7.0.3 - '@octokit/graphql@9.0.3': + '@octokit/graphql@9.0.5': dependencies: - '@octokit/request': 10.0.11 - '@octokit/types': 16.0.0 + '@octokit/request': 10.0.16 + '@octokit/types': 18.0.0 universal-user-agent: 7.0.3 '@octokit/openapi-types@27.0.0': {} - '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.6)': + '@octokit/openapi-types@28.0.0': {} + + '@octokit/openapi-types@29.0.1': {} + + '@octokit/plugin-paginate-rest@14.0.0(@octokit/core@7.0.8)': dependencies: - '@octokit/core': 7.0.6 + '@octokit/core': 7.0.8 '@octokit/types': 16.0.0 - '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.6)': + '@octokit/plugin-rest-endpoint-methods@17.0.0(@octokit/core@7.0.8)': dependencies: - '@octokit/core': 7.0.6 + '@octokit/core': 7.0.8 '@octokit/types': 16.0.0 - '@octokit/plugin-retry@8.1.0(@octokit/core@7.0.6)': + '@octokit/plugin-retry@8.1.1(@octokit/core@7.0.8)': dependencies: - '@octokit/core': 7.0.6 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 + '@octokit/core': 7.0.8 + '@octokit/request-error': 7.1.2 + '@octokit/types': 17.0.0 bottleneck: 2.19.5 - '@octokit/request-error@7.1.0': + '@octokit/request-error@7.1.2': dependencies: - '@octokit/types': 16.0.0 + '@octokit/types': 18.0.0 - '@octokit/request@10.0.11': + '@octokit/request@10.0.16': dependencies: - '@octokit/endpoint': 11.0.3 - '@octokit/request-error': 7.1.0 - '@octokit/types': 16.0.0 - content-type: 2.0.0 - json-with-bigint: 3.5.8 + '@octokit/endpoint': 11.0.5 + '@octokit/request-error': 7.1.2 + '@octokit/types': 18.0.0 + content-type: 3.1.1 + json-with-bigint: 3.5.12 universal-user-agent: 7.0.3 '@octokit/types@16.0.0': dependencies: '@octokit/openapi-types': 27.0.0 - '@oven/bun-darwin-aarch64@1.3.14': - optional: true - - '@oven/bun-darwin-x64-baseline@1.3.14': - optional: true - - '@oven/bun-darwin-x64@1.3.14': - optional: true + '@octokit/types@17.0.0': + dependencies: + '@octokit/openapi-types': 28.0.0 - '@oven/bun-freebsd-aarch64@1.3.14': - optional: true + '@octokit/types@18.0.0': + dependencies: + '@octokit/openapi-types': 29.0.1 - '@oven/bun-freebsd-x64@1.3.14': + '@oven/bun-darwin-aarch64@1.4.2': optional: true - '@oven/bun-linux-aarch64-android@1.3.14': + '@oven/bun-darwin-x64@1.4.2': optional: true - '@oven/bun-linux-aarch64-musl@1.3.14': + '@oven/bun-freebsd-aarch64@1.4.2': optional: true - '@oven/bun-linux-aarch64@1.3.14': + '@oven/bun-freebsd-x64@1.4.2': optional: true - '@oven/bun-linux-x64-android@1.3.14': + '@oven/bun-linux-aarch64-android@1.4.2': optional: true - '@oven/bun-linux-x64-baseline@1.3.14': + '@oven/bun-linux-aarch64-musl@1.4.2': optional: true - '@oven/bun-linux-x64-musl-baseline@1.3.14': + '@oven/bun-linux-aarch64@1.4.2': optional: true - '@oven/bun-linux-x64-musl@1.3.14': + '@oven/bun-linux-x64-android@1.4.2': optional: true - '@oven/bun-linux-x64@1.3.14': + '@oven/bun-linux-x64-musl@1.4.2': optional: true - '@oven/bun-windows-aarch64@1.3.14': + '@oven/bun-linux-x64@1.4.2': optional: true - '@oven/bun-windows-x64-baseline@1.3.14': + '@oven/bun-windows-aarch64@1.4.2': optional: true - '@oven/bun-windows-x64@1.3.14': + '@oven/bun-windows-x64@1.4.2': optional: true - '@oxc-project/types@0.138.0': {} + '@oxc-project/types@0.151.0': {} '@oxfmt/binding-android-arm-eabi@0.35.0': optional: true @@ -3385,57 +3781,53 @@ snapshots: '@pkgr/core@0.3.6': {} - '@quansync/fs@1.0.0': + '@quansync/fs@1.1.0': dependencies: quansync: 1.0.0 - '@rolldown/binding-android-arm64@1.1.4': + '@rolldown/binding-android-arm-eabi@1.2.10': optional: true - '@rolldown/binding-darwin-arm64@1.1.4': + '@rolldown/binding-android-arm64@1.2.10': optional: true - '@rolldown/binding-darwin-x64@1.1.4': + '@rolldown/binding-darwin-arm64@1.2.10': optional: true - '@rolldown/binding-freebsd-x64@1.1.4': + '@rolldown/binding-darwin-x64@1.2.10': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.4': + '@rolldown/binding-freebsd-x64@1.2.10': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.4': + '@rolldown/binding-linux-arm-gnueabihf@1.2.10': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.4': + '@rolldown/binding-linux-arm64-gnu@1.2.10': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.4': + '@rolldown/binding-linux-arm64-musl@1.2.10': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.4': + '@rolldown/binding-linux-ppc64-gnu@1.2.10': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.4': + '@rolldown/binding-linux-s390x-gnu@1.2.10': optional: true - '@rolldown/binding-linux-x64-musl@1.1.4': + '@rolldown/binding-linux-x64-gnu@1.2.10': optional: true - '@rolldown/binding-openharmony-arm64@1.1.4': + '@rolldown/binding-linux-x64-musl@1.2.10': optional: true - '@rolldown/binding-wasm32-wasi@1.1.4': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-openharmony-arm64@1.2.10': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.4': + '@rolldown/binding-win32-arm64-msvc@1.2.10': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.4': + '@rolldown/binding-win32-x64-msvc@1.2.10': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -3450,6 +3842,10 @@ snapshots: dependencies: '@simple-libs/stream-utils': 1.2.0 + '@simple-libs/child-process-utils@2.0.0': + dependencies: + '@simple-libs/stream-utils': 2.0.0 + '@simple-libs/stream-utils@1.2.0': {} '@simple-libs/stream-utils@2.0.0': {} @@ -3458,25 +3854,36 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@standard-schema/spec@1.1.0': {} - - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.4)': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier@3.9.9)(supports-color@10.2.2)': dependencies: - '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.9 + '@babel/traverse': 7.29.8(supports-color@10.2.2) + '@babel/types': 7.29.8 javascript-natural-sort: 0.7.1 lodash-es: 4.18.1 minimatch: 9.0.9 parse-imports-exports: 0.2.4 - prettier: 3.9.4 + prettier: 3.9.9 transitivePeerDependencies: - supports-color - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 + '@turbo/darwin-64@2.11.3': + optional: true + + '@turbo/darwin-arm64@2.11.3': + optional: true + + '@turbo/linux-64@2.11.3': + optional: true + + '@turbo/linux-arm64@2.11.3': + optional: true + + '@turbo/windows-64@2.11.3': + optional: true + + '@turbo/windows-arm64@2.11.3': optional: true '@types/chai@5.2.3': @@ -3500,88 +3907,86 @@ snapshots: '@types/through': 0.0.33 rxjs: 7.8.2 - '@types/jsesc@2.5.1': {} - '@types/json-schema@7.0.15': {} - '@types/node@26.1.0': + '@types/node@26.6.2': dependencies: - undici-types: 8.3.0 + undici-types: 8.9.0 '@types/through@0.0.33': dependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 '@types/validator@13.15.10': {} - '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.62.1 - eslint: 10.6.0(jiti@2.6.1) - ignore: 7.0.5 + '@typescript-eslint/parser': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/type-utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.1 + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) + ignore: 7.0.10 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.62.1 - debug: 4.4.3 - eslint: 10.6.0(jiti@2.6.1) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': + '@typescript-eslint/project-service@8.70.1(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) - '@typescript-eslint/types': 8.62.1 - debug: 4.4.3 + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) + '@typescript-eslint/types': 8.70.1 + debug: 4.4.3(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.62.1': + '@typescript-eslint/scope-manager@8.70.1': dependencies: - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/visitor-keys': 8.62.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 - '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.70.1(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - debug: 4.4.3 - eslint: 10.6.0(jiti@2.6.1) + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.62.1': {} + '@typescript-eslint/types@8.70.1': {} - '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.70.1(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/visitor-keys': 8.62.1 - debug: 4.4.3 - minimatch: 10.2.5 + '@typescript-eslint/project-service': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.70.1(typescript@6.0.3) + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/visitor-keys': 8.70.1 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -3589,82 +3994,130 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/utils@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.62.1 - '@typescript-eslint/types': 8.62.1 - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - eslint: 10.6.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)) + '@typescript-eslint/scope-manager': 8.70.1 + '@typescript-eslint/types': 8.70.1 + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.62.1': + '@typescript-eslint/visitor-keys@8.70.1': dependencies: - '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/types': 8.70.1 eslint-visitor-keys: 5.0.1 - '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + '@vitest/coverage-v8@5.0.1(vitest@5.0.1)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.10 - ast-v8-to-istanbul: 1.0.4 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.2.0 - magicast: 0.5.3 - obug: 2.1.3 - std-env: 4.1.0 - tinyrainbow: 3.1.0 - vitest: 4.1.10(@types/node@26.1.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)) - - '@vitest/expect@4.1.10': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.0 + '@vitest/istanbul-lib-coverage': 1.0.1 + '@vitest/istanbul-lib-report': 1.0.1 + ast-v8-to-istanbul: 1.0.7 + magicast: 0.5.5 + obug: 2.2.1 + std-env: 4.2.0 + tinyrainbow: 3.1.1 + vitest: 5.0.1(@types/node@26.6.2)(@vitest/coverage-v8@5.0.1)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1)) + + '@vitest/istanbul-lib-coverage@1.0.1': {} - '@vitest/mocker@4.1.10(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0))': + '@vitest/istanbul-lib-report@1.0.1': dependencies: - '@vitest/spy': 4.1.10 + '@vitest/istanbul-lib-coverage': 1.0.1 + + '@vitest/mocker@5.0.1(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1))': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.1 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.4.2 optionalDependencies: - vite: 8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1) - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.0 + '@vitest/spy@5.0.1': {} - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 + '@yuku-codegen/binding-android-arm64@0.10.2': + optional: true - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 + '@yuku-codegen/binding-darwin-arm64@0.10.2': + optional: true - '@vitest/spy@4.1.10': {} + '@yuku-codegen/binding-darwin-x64@0.10.2': + optional: true - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.0 + '@yuku-codegen/binding-freebsd-x64@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm-gnu@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm-musl@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm64-gnu@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-arm64-musl@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-x64-gnu@0.10.2': + optional: true + + '@yuku-codegen/binding-linux-x64-musl@0.10.2': + optional: true + + '@yuku-codegen/binding-win32-arm64@0.10.2': + optional: true + + '@yuku-codegen/binding-win32-x64@0.10.2': + optional: true + + '@yuku-parser/binding-android-arm64@0.10.2': + optional: true + + '@yuku-parser/binding-darwin-arm64@0.10.2': + optional: true + + '@yuku-parser/binding-darwin-x64@0.10.2': + optional: true + + '@yuku-parser/binding-freebsd-x64@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm-gnu@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm-musl@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm64-gnu@0.10.2': + optional: true + + '@yuku-parser/binding-linux-arm64-musl@0.10.2': + optional: true + + '@yuku-parser/binding-linux-x64-gnu@0.10.2': + optional: true + + '@yuku-parser/binding-linux-x64-musl@0.10.2': + optional: true + + '@yuku-parser/binding-win32-arm64@0.10.2': + optional: true + + '@yuku-parser/binding-win32-x64@0.10.2': + optional: true + + '@yuku-toolchain/types@0.10.2': {} - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: @@ -3680,7 +4133,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -3688,23 +4141,19 @@ snapshots: dependencies: environment: 1.1.0 - ansi-regex@6.2.2: {} + ansi-regex@6.3.0: {} ansi-styles@6.2.3: {} - ansis@4.3.1: {} + ansis@4.4.0: {} argparse@2.0.1: {} - assertion-error@2.0.1: {} + argue-cli@3.2.0: {} - ast-kit@3.0.0: - dependencies: - '@babel/parser': 8.0.0 - estree-walker: 3.0.3 - pathe: 2.0.3 + assertion-error@2.0.1: {} - ast-v8-to-istanbul@1.0.4: + ast-v8-to-istanbul@1.0.7: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 @@ -3716,41 +4165,34 @@ snapshots: before-after-hook@4.0.0: {} - birpc@4.0.0: {} - bottleneck@2.19.5: {} - brace-expansion@2.1.1: + brace-expansion@2.1.7: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.7: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 braces@3.0.3: dependencies: fill-range: 7.1.1 - optional: true - bun@1.3.14: + bun@1.4.2: optionalDependencies: - '@oven/bun-darwin-aarch64': 1.3.14 - '@oven/bun-darwin-x64': 1.3.14 - '@oven/bun-darwin-x64-baseline': 1.3.14 - '@oven/bun-freebsd-aarch64': 1.3.14 - '@oven/bun-freebsd-x64': 1.3.14 - '@oven/bun-linux-aarch64': 1.3.14 - '@oven/bun-linux-aarch64-android': 1.3.14 - '@oven/bun-linux-aarch64-musl': 1.3.14 - '@oven/bun-linux-x64': 1.3.14 - '@oven/bun-linux-x64-android': 1.3.14 - '@oven/bun-linux-x64-baseline': 1.3.14 - '@oven/bun-linux-x64-musl': 1.3.14 - '@oven/bun-linux-x64-musl-baseline': 1.3.14 - '@oven/bun-windows-aarch64': 1.3.14 - '@oven/bun-windows-x64': 1.3.14 - '@oven/bun-windows-x64-baseline': 1.3.14 + '@oven/bun-darwin-aarch64': 1.4.2 + '@oven/bun-darwin-x64': 1.4.2 + '@oven/bun-freebsd-aarch64': 1.4.2 + '@oven/bun-freebsd-x64': 1.4.2 + '@oven/bun-linux-aarch64': 1.4.2 + '@oven/bun-linux-aarch64-android': 1.4.2 + '@oven/bun-linux-aarch64-musl': 1.4.2 + '@oven/bun-linux-x64': 1.4.2 + '@oven/bun-linux-x64-android': 1.4.2 + '@oven/bun-linux-x64-musl': 1.4.2 + '@oven/bun-windows-aarch64': 1.4.2 + '@oven/bun-windows-x64': 1.4.2 bundle-name@4.1.0: dependencies: @@ -3758,6 +4200,14 @@ snapshots: cac@7.0.0: {} + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + callsites@3.1.0: {} chai@6.2.2: {} @@ -3770,17 +4220,16 @@ snapshots: chokidar@5.0.0: dependencies: - readdirp: 5.0.0 + readdirp: 5.1.1 - chownr@3.0.0: - optional: true + chownr@3.0.0: {} class-transformer@0.5.1: {} class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.13.8 + libphonenumber-js: 1.13.13 validator: 13.15.35 cli-cursor@5.0.0: @@ -3789,11 +4238,6 @@ snapshots: cli-spinners@3.4.0: {} - cli-truncate@5.2.0: - dependencies: - slice-ansi: 8.0.0 - string-width: 8.2.1 - cli-width@4.1.0: {} cliui@9.0.1: @@ -3808,15 +4252,15 @@ snapshots: commander@15.0.0: {} - content-type@2.0.0: {} + content-type@3.1.1: {} - conventional-changelog-angular@9.2.1: + conventional-changelog-angular@9.4.0: dependencies: - '@conventional-changelog/template': 1.2.1 + '@conventional-changelog/template': 1.4.0 - conventional-changelog-conventionalcommits@10.2.1: + conventional-changelog-conventionalcommits@10.4.0: dependencies: - '@conventional-changelog/template': 1.2.1 + '@conventional-changelog/template': 1.4.0 conventional-changelog-preset-loader@5.0.0: {} @@ -3827,10 +4271,10 @@ snapshots: '@simple-libs/stream-utils': 1.2.0 meow: 13.2.0 - conventional-commits-parser@7.0.1: + conventional-commits-parser@7.1.2: dependencies: '@simple-libs/stream-utils': 2.0.0 - meow: 14.1.0 + argue-cli: 3.2.0 conventional-recommended-bump@11.2.0: dependencies: @@ -3840,11 +4284,9 @@ snapshots: conventional-commits-parser: 6.4.0 meow: 13.2.0 - convert-source-map@2.0.0: {} - - cosmiconfig-typescript-loader@6.3.0(@types/node@26.1.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): + cosmiconfig-typescript-loader@6.3.0(@types/node@26.6.2)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): dependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 cosmiconfig: 9.0.2(typescript@6.0.3) jiti: 2.6.1 typescript: 6.0.3 @@ -3853,26 +4295,41 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.0 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 + cpx2@9.0.3: + dependencies: + debounce: 3.0.0 + glob: 13.0.6 + glob2base: 0.0.12 + ignore: 7.0.10 + minimatch: 10.2.6 + p-map: 7.0.8 + shell-quote: 1.10.0 + subarg: 1.0.0 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3: + debounce@3.0.0: {} + + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 deep-is@0.1.4: {} default-browser-id@5.0.1: {} - default-browser@5.5.0: + default-browser@5.5.1: dependencies: bundle-name: 4.1.0 default-browser-id: 5.0.1 @@ -3887,13 +4344,15 @@ snapshots: dotenv@17.4.2: {} + dotenv@18.0.3: {} + dts-resolver@3.0.0: {} emoji-regex@10.6.0: {} emojilib@2.4.0: {} - empathic@2.0.1: {} + empathic@2.1.0: {} env-paths@2.2.1: {} @@ -3903,17 +4362,17 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-module-lexer@2.3.0: {} + es-module-lexer@2.3.2: {} - es-toolkit@1.49.0: {} + es-toolkit@1.52.0: {} escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.6.1)): + eslint-config-prettier@10.1.8(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)): dependencies: - eslint: 10.6.0(jiti@2.6.1) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) eslint-formatter-pretty@7.1.0: dependencies: @@ -3923,38 +4382,38 @@ snapshots: eslint-rule-docs: 1.1.235 log-symbols: 7.0.1 plur: 5.1.0 - string-width: 8.2.1 + string-width: 8.2.2 supports-hyperlinks: 4.5.0 - eslint-formatting-reporter@0.0.0(eslint@10.6.0(jiti@2.6.1)): + eslint-formatting-reporter@0.0.0(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)): dependencies: - eslint: 10.6.0(jiti@2.6.1) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) prettier-linter-helpers: 1.0.1 eslint-parser-plain@0.1.1: {} - eslint-plugin-format@1.5.0(eslint@10.6.0(jiti@2.6.1)): + eslint-plugin-format@1.5.0(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)): dependencies: '@dprint/formatter': 0.5.1 '@dprint/markdown': 0.21.1 '@dprint/toml': 0.7.0 - eslint: 10.6.0(jiti@2.6.1) - eslint-formatting-reporter: 0.0.0(eslint@10.6.0(jiti@2.6.1)) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) + eslint-formatting-reporter: 0.0.0(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)) eslint-parser-plain: 0.1.1 - ohash: 2.0.11 + ohash: 2.0.12 oxfmt: 0.35.0 - prettier: 3.9.4 + prettier: 3.9.9 synckit: 0.11.13 - eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.6.0(jiti@2.6.1)))(eslint@10.6.0(jiti@2.6.1))(prettier@3.9.4): + eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)))(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(prettier@3.9.9): dependencies: - eslint: 10.6.0(jiti@2.6.1) - prettier: 3.9.4 + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) + prettier: 3.9.9 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@10.6.0(jiti@2.6.1)) + eslint-config-prettier: 10.1.8(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)) eslint-rule-docs@1.1.235: {} @@ -3969,21 +4428,21 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0(jiti@2.6.1): + eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@2.6.1)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 + '@eslint/config-array': 0.23.5(supports-color@10.2.2) + '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.2 + '@eslint/plugin-kit': 0.7.3 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -3991,14 +4450,14 @@ snapshots: esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 + file-entry-cache: 11.1.5 find-up: 5.0.0 glob-parent: 6.0.2 ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -4008,8 +4467,8 @@ snapshots: espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esquery@1.7.0: @@ -4028,8 +4487,6 @@ snapshots: esutils@2.0.3: {} - eventemitter3@5.0.4: {} - execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -4040,10 +4497,10 @@ snapshots: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.3.0 + pretty-ms: 9.3.1 signal-exit: 4.1.0 strip-final-newline: 4.0.0 - yoctocolors: 2.1.2 + yoctocolors: 2.2.0 expect-type@1.4.0: {} @@ -4061,111 +4518,119 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.3: {} + fast-uri@3.1.8: {} fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@8.0.0: + file-entry-cache@11.1.5: dependencies: - flat-cache: 4.0.1 + flat-cache: 6.1.23 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - optional: true + + find-index@0.1.1: {} find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - flat-cache@4.0.1: + flat-cache@6.1.23: dependencies: - flatted: 3.4.2 - keyv: 4.5.4 + cacheable: 2.5.0 + flatted: 3.4.4 + hookified: 1.15.1 - flatted@3.4.2: {} + flatted@3.4.4: {} fsevents@2.3.3: optional: true get-caller-file@2.0.5: {} - get-east-asian-width@1.6.0: {} + get-east-asian-width@1.7.0: {} get-stream@9.0.1: dependencies: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - get-tsconfig@5.0.0-beta.5: + get-tsconfig@5.0.0-beta.6: dependencies: resolve-pkg-maps: 1.0.0 - git-cliff-darwin-arm64@2.13.1: + git-cliff-darwin-arm64@2.14.2: optional: true - git-cliff-darwin-x64@2.13.1: + git-cliff-darwin-x64@2.14.2: optional: true - git-cliff-linux-arm64@2.13.1: + git-cliff-linux-arm64@2.14.2: optional: true - git-cliff-linux-x64@2.13.1: + git-cliff-linux-x64@2.14.2: optional: true - git-cliff-windows-arm64@2.13.1: + git-cliff-windows-arm64@2.14.2: optional: true - git-cliff-windows-x64@2.13.1: + git-cliff-windows-x64@2.14.2: optional: true - git-cliff@2.13.1: + git-cliff@2.14.2: dependencies: execa: 9.6.1 optionalDependencies: - git-cliff-darwin-arm64: 2.13.1 - git-cliff-darwin-x64: 2.13.1 - git-cliff-linux-arm64: 2.13.1 - git-cliff-linux-x64: 2.13.1 - git-cliff-windows-arm64: 2.13.1 - git-cliff-windows-x64: 2.13.1 - - git-raw-commits@5.0.1(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0): - dependencies: - '@conventional-changelog/git-client': 2.7.0(conventional-commits-filter@5.0.0)(conventional-commits-parser@6.4.0) - meow: 13.2.0 - transitivePeerDependencies: - - conventional-commits-filter - - conventional-commits-parser + git-cliff-darwin-arm64: 2.14.2 + git-cliff-darwin-x64: 2.14.2 + git-cliff-linux-arm64: 2.14.2 + git-cliff-linux-x64: 2.14.2 + git-cliff-windows-arm64: 2.14.2 + git-cliff-windows-x64: 2.14.2 glob-parent@6.0.2: dependencies: is-glob: 4.0.3 + glob2base@0.0.12: + dependencies: + find-index: 0.1.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + global-directory@5.0.0: dependencies: ini: 6.0.0 globals@16.5.0: {} - has-flag@4.0.0: {} - has-flag@5.0.1: {} - hookable@6.1.1: {} + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hookable@6.1.2: {} + + hookified@1.15.1: {} - html-escaper@2.0.2: {} + hookified@2.2.0: {} human-signals@8.0.1: {} @@ -4177,14 +4642,14 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.10: {} import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-without-cache@0.4.0: {} + import-without-cache@0.4.1: {} imurmurhash@0.1.4: {} @@ -4198,10 +4663,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@5.1.0: - dependencies: - get-east-asian-width: 1.6.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -4214,8 +4675,7 @@ snapshots: is-interactive@2.0.0: {} - is-number@7.0.0: - optional: true + is-number@7.0.0: {} is-plain-obj@4.1.0: {} @@ -4229,19 +4689,6 @@ snapshots: isexe@2.0.0: {} - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - javascript-natural-sort@0.7.1: {} jiti@2.6.1: {} @@ -4250,14 +4697,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} json-schema-traverse@0.4.1: {} @@ -4266,88 +4711,79 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-with-bigint@3.5.8: {} + json-with-bigint@3.5.12: {} jsonc-parser@3.3.1: {} - keyv@4.5.4: + keyv@5.6.0: dependencies: - json-buffer: 3.0.1 + '@keyv/serialize': 1.1.1 levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.13.8: {} + libphonenumber-js@1.13.13: {} - lightningcss-android-arm64@1.32.0: + lightningcss-android-arm64@1.33.0: optional: true - lightningcss-darwin-arm64@1.32.0: + lightningcss-darwin-arm64@1.33.0: optional: true - lightningcss-darwin-x64@1.32.0: + lightningcss-darwin-x64@1.33.0: optional: true - lightningcss-freebsd-x64@1.32.0: + lightningcss-freebsd-x64@1.33.0: optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true - lightningcss-linux-arm64-gnu@1.32.0: + lightningcss-linux-arm64-gnu@1.33.0: optional: true - lightningcss-linux-arm64-musl@1.32.0: + lightningcss-linux-arm64-musl@1.33.0: optional: true - lightningcss-linux-x64-gnu@1.32.0: + lightningcss-linux-x64-gnu@1.33.0: optional: true - lightningcss-linux-x64-musl@1.32.0: + lightningcss-linux-x64-musl@1.33.0: optional: true - lightningcss-win32-arm64-msvc@1.32.0: + lightningcss-win32-arm64-msvc@1.33.0: optional: true - lightningcss-win32-x64-msvc@1.32.0: + lightningcss-win32-x64-msvc@1.33.0: optional: true - lightningcss@1.32.0: + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-android-arm64: 1.32.0 - lightningcss-darwin-arm64: 1.32.0 - lightningcss-darwin-x64: 1.32.0 - lightningcss-freebsd-x64: 1.32.0 - lightningcss-linux-arm-gnueabihf: 1.32.0 - lightningcss-linux-arm64-gnu: 1.32.0 - lightningcss-linux-arm64-musl: 1.32.0 - lightningcss-linux-x64-gnu: 1.32.0 - lightningcss-linux-x64-musl: 1.32.0 - lightningcss-win32-arm64-msvc: 1.32.0 - lightningcss-win32-x64-msvc: 1.32.0 + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 lines-and-columns@1.2.4: {} - lint-staged@17.0.8: + lint-staged@17.5.1: dependencies: - listr2: 10.2.2 - picomatch: 4.0.5 + picomatch: 4.0.7 string-argv: 0.3.2 - tinyexec: 1.2.4 + tinyexec: 1.3.1 optionalDependencies: - yaml: 2.9.0 - - listr2@10.2.2: - dependencies: - cli-truncate: 5.2.0 - eventemitter3: 5.0.4 - log-update: 6.1.0 - rfdc: 1.4.1 - wrap-ansi: 10.0.0 + yaml: 2.9.1 locate-path@6.0.0: dependencies: @@ -4358,63 +4794,54 @@ snapshots: log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 - yoctocolors: 2.1.2 + yoctocolors: 2.2.0 - log-update@6.1.0: - dependencies: - ansi-escapes: 7.3.0 - cli-cursor: 5.0.0 - slice-ansi: 7.1.2 - strip-ansi: 7.2.0 - wrap-ansi: 9.0.2 + lru-cache@11.5.3: {} - magic-string@0.30.21: + magic-string@1.0.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - magicast@0.5.3: + magic-string@1.4.2: dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - source-map-js: 1.2.1 + '@jridgewell/sourcemap-codec': 1.6.0 - make-dir@4.0.0: + magicast@0.5.5: dependencies: - semver: 7.8.5 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + source-map-js: 1.2.1 meow@13.2.0: {} - meow@14.1.0: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.2 - optional: true mimic-function@5.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.12 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.1 + brace-expansion: 2.1.7 - minipass@7.1.3: - optional: true + minimist@1.2.8: {} + + minipass@7.1.3: {} minizlib@3.1.0: dependencies: minipass: 7.1.3 - optional: true ms@2.1.3: {} mute-stream@3.0.0: {} - nanoid@3.3.15: {} + nanoid@3.3.19: {} natural-compare@1.4.0: {} @@ -4430,22 +4857,24 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - obug@2.1.3: {} + obug@2.2.1: {} + + obug@3.0.0: {} - ohash@2.0.11: {} + ohash@2.0.12: {} onetime@7.0.0: dependencies: mimic-function: 5.0.1 - open@11.0.0: + open@11.0.4: dependencies: - default-browser: 5.5.0 + default-browser: 5.5.1 define-lazy-prop: 3.0.0 is-in-ssh: 1.0.0 is-inside-container: 1.0.0 - powershell-utils: 0.1.0 - wsl-utils: 0.3.1 + powershell-utils: 0.2.1 + wsl-utils: 1.0.0 optionator@0.9.4: dependencies: @@ -4456,17 +4885,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@9.4.0: - dependencies: - chalk: 5.6.2 - cli-cursor: 5.0.0 - cli-spinners: 3.4.0 - is-interactive: 2.0.0 - is-unicode-supported: 2.1.0 - log-symbols: 7.0.1 - stdin-discarder: 0.3.2 - string-width: 8.2.1 - ora@9.4.1: dependencies: chalk: 5.6.2 @@ -4476,7 +4894,7 @@ snapshots: is-unicode-supported: 2.1.0 log-symbols: 7.0.1 stdin-discarder: 0.3.2 - string-width: 8.2.1 + string-width: 8.2.2 oxfmt@0.35.0: dependencies: @@ -4510,6 +4928,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-map@7.0.8: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -4535,51 +4955,59 @@ snapshots: path-key@4.0.0: {} - pathe@2.0.3: {} + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 picocolors@1.1.1: {} - picomatch@2.3.2: - optional: true - - picomatch@4.0.4: {} + picomatch@2.3.2: {} picomatch@4.0.5: {} + picomatch@4.0.7: {} + plur@5.1.0: dependencies: irregular-plurals: 3.5.0 - postcss@8.5.16: + postcss@8.5.28: dependencies: - nanoid: 3.3.15 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 powershell-utils@0.1.0: {} + powershell-utils@0.2.1: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier@3.9.4: {} + prettier@3.9.9: {} - pretty-ms@9.3.0: + pretty-ms@9.3.1: dependencies: parse-ms: 4.0.0 punycode@2.3.1: {} + qified@0.10.1: + dependencies: + hookified: 2.2.0 + quansync@1.0.0: {} - rc9@3.0.1: + rc9@3.1.0: dependencies: defu: 6.1.7 destr: 2.0.5 - readdirp@5.0.0: {} + readdirp@5.1.1: {} reflect-metadata@0.2.2: {} @@ -4596,44 +5024,40 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 - rfdc@1.4.1: {} - - rolldown-plugin-dts@0.26.0(rolldown@1.1.4)(typescript@6.0.3): + rolldown-plugin-dts@0.28.6(rolldown@1.2.10)(typescript@6.0.3): dependencies: - '@babel/generator': 8.0.0 - '@babel/helper-validator-identifier': 8.0.2 - '@babel/parser': 8.0.0 - ast-kit: 3.0.0 - birpc: 4.0.0 dts-resolver: 3.0.0 - get-tsconfig: 5.0.0-beta.5 - obug: 2.1.3 - rolldown: 1.1.4 + get-tsconfig: 5.0.0-beta.6 + obug: 3.0.0 + rolldown: 1.2.10 + yuku-ast: 0.10.2 + yuku-codegen: 0.10.2 + yuku-parser: 0.10.2 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - oxc-resolver - rolldown@1.1.4: + rolldown@1.2.10: dependencies: - '@oxc-project/types': 0.138.0 + '@oxc-project/types': 0.151.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.4 - '@rolldown/binding-darwin-arm64': 1.1.4 - '@rolldown/binding-darwin-x64': 1.1.4 - '@rolldown/binding-freebsd-x64': 1.1.4 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.4 - '@rolldown/binding-linux-arm64-gnu': 1.1.4 - '@rolldown/binding-linux-arm64-musl': 1.1.4 - '@rolldown/binding-linux-ppc64-gnu': 1.1.4 - '@rolldown/binding-linux-s390x-gnu': 1.1.4 - '@rolldown/binding-linux-x64-gnu': 1.1.4 - '@rolldown/binding-linux-x64-musl': 1.1.4 - '@rolldown/binding-openharmony-arm64': 1.1.4 - '@rolldown/binding-wasm32-wasi': 1.1.4 - '@rolldown/binding-win32-arm64-msvc': 1.1.4 - '@rolldown/binding-win32-x64-msvc': 1.1.4 + '@rolldown/binding-android-arm-eabi': 1.2.10 + '@rolldown/binding-android-arm64': 1.2.10 + '@rolldown/binding-darwin-arm64': 1.2.10 + '@rolldown/binding-darwin-x64': 1.2.10 + '@rolldown/binding-freebsd-x64': 1.2.10 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.10 + '@rolldown/binding-linux-arm64-gnu': 1.2.10 + '@rolldown/binding-linux-arm64-musl': 1.2.10 + '@rolldown/binding-linux-ppc64-gnu': 1.2.10 + '@rolldown/binding-linux-s390x-gnu': 1.2.10 + '@rolldown/binding-linux-x64-gnu': 1.2.10 + '@rolldown/binding-linux-x64-musl': 1.2.10 + '@rolldown/binding-openharmony-arm64': 1.2.10 + '@rolldown/binding-win32-arm64-msvc': 1.2.10 + '@rolldown/binding-win32-x64-msvc': 1.2.10 run-applescript@7.1.0: {} @@ -4651,6 +5075,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.10.0: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -4659,17 +5085,7 @@ snapshots: dependencies: unicode-emoji-modifier-base: 1.0.0 - slice-ansi@7.1.2: - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - slice-ansi@8.0.0: - dependencies: - ansi-styles: 6.2.3 - is-fullwidth-code-point: 5.1.0 - - smol-toml@1.7.0: {} + smol-toml@1.9.0: {} source-map-js@1.2.1: {} @@ -4677,7 +5093,7 @@ snapshots: stackback@0.0.2: {} - std-env@4.1.0: {} + std-env@4.2.0: {} stdin-discarder@0.3.2: {} @@ -4686,25 +5102,25 @@ snapshots: string-width@7.2.0: dependencies: emoji-regex: 10.6.0 - get-east-asian-width: 1.6.0 + get-east-asian-width: 1.7.0 strip-ansi: 7.2.0 - string-width@8.2.1: + string-width@8.2.2: dependencies: - get-east-asian-width: 1.6.0 + get-east-asian-width: 1.7.0 strip-ansi: 7.2.0 strip-ansi@7.2.0: dependencies: - ansi-regex: 6.2.2 + ansi-regex: 6.3.0 strip-final-newline@4.0.0: {} - supports-color@10.2.2: {} - - supports-color@7.2.0: + subarg@1.0.0: dependencies: - has-flag: 4.0.0 + minimist: 1.2.8 + + supports-color@10.2.2: {} supports-hyperlinks@4.5.0: dependencies: @@ -4715,32 +5131,32 @@ snapshots: dependencies: '@pkgr/core': 0.3.6 - tar@7.5.19: + tar@7.5.22: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 - optional: true - tinybench@2.9.0: {} + tinybench@6.1.4: {} + + tinyexec@1.3.0: {} - tinyexec@1.2.4: {} + tinyexec@1.3.1: {} tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 tinypool@2.1.0: {} - tinyrainbow@3.1.0: {} + tinyrainbow@3.1.1: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - optional: true tree-kill@1.2.2: {} @@ -4748,28 +5164,29 @@ snapshots: dependencies: typescript: 6.0.3 - tsdown@0.22.3(typescript@6.0.3): + tsdown@0.23.0(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)): dependencies: - ansis: 4.3.1 cac: 7.0.0 defu: 6.1.7 - empathic: 2.0.1 - hookable: 6.1.1 - import-without-cache: 0.4.0 - obug: 2.1.3 - picomatch: 4.0.5 - rolldown: 1.1.4 - rolldown-plugin-dts: 0.26.0(rolldown@1.1.4)(typescript@6.0.3) - semver: 7.8.5 - tinyexec: 1.2.4 + empathic: 2.1.0 + hookable: 6.1.2 + import-without-cache: 0.4.1 + obug: 2.2.1 + picomatch: 4.0.7 + rolldown: 1.2.10 + rolldown-plugin-dts: 0.28.6(rolldown@1.2.10)(typescript@6.0.3) + tinyexec: 1.3.1 tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.5.0 + verkit: 0.4.1 optionalDependencies: typescript: 6.0.3 + unrun: 0.3.1(synckit@0.11.13) transitivePeerDependencies: - - '@ts-macro/tsc' - '@typescript/native-preview' + - '@volar/typescript' + - '@vue/language-core' - oxc-resolver - vue-tsc @@ -4777,17 +5194,26 @@ snapshots: tunnel@0.0.6: {} + turbo@2.11.3: + optionalDependencies: + '@turbo/darwin-64': 2.11.3 + '@turbo/darwin-arm64': 2.11.3 + '@turbo/linux-64': 2.11.3 + '@turbo/linux-arm64': 2.11.3 + '@turbo/windows-64': 2.11.3 + '@turbo/windows-arm64': 2.11.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3): + typescript-eslint@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) - '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@2.6.1))(typescript@6.0.3) - eslint: 10.6.0(jiti@2.6.1) + '@typescript-eslint/eslint-plugin': 8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3))(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.70.1(supports-color@10.2.2)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.1(eslint@10.11.0(jiti@2.6.1)(supports-color@10.2.2))(supports-color@10.2.2)(typescript@6.0.3) + eslint: 10.11.0(jiti@2.6.1)(supports-color@10.2.2) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4796,12 +5222,12 @@ snapshots: unconfig-core@7.5.0: dependencies: - '@quansync/fs': 1.0.0 + '@quansync/fs': 1.1.0 quansync: 1.0.0 - undici-types@8.3.0: {} + undici-types@8.9.0: {} - undici@6.27.0: {} + undici@6.28.1: {} unicode-emoji-modifier-base@1.0.0: {} @@ -4809,50 +5235,52 @@ snapshots: universal-user-agent@7.0.3: {} + unrun@0.3.1(synckit@0.11.13): + dependencies: + rolldown: 1.2.10 + optionalDependencies: + synckit: 0.11.13 + uri-js@4.4.1: dependencies: punycode: 2.3.1 validator@13.15.35: {} - vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0): + verkit@0.4.1: {} + + vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.5 - postcss: 8.5.16 - rolldown: 1.1.4 + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.10 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.1.0 + '@types/node': 26.6.2 fsevents: 2.3.3 jiti: 2.6.1 - yaml: 2.9.0 - - vitest@4.1.10(@types/node@26.1.0)(@vitest/coverage-v8@4.1.10)(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.0 + yaml: 2.9.1 + + vitest@5.0.1(@types/node@26.6.2)(@vitest/coverage-v8@5.0.1)(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1)): + dependencies: + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1)) + chai: 6.2.2 + es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.3 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.1.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 + magic-string: 1.4.2 + obug: 2.2.1 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 6.1.4 + tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.1.2(@types/node@26.1.0)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.6.2)(jiti@2.6.1)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 26.1.0 - '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + '@types/node': 26.6.2 + '@vitest/coverage-v8': 5.0.1(vitest@5.0.1) transitivePeerDependencies: - msw @@ -4867,42 +5295,76 @@ snapshots: word-wrap@1.2.5: {} - wrap-ansi@10.0.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 8.2.1 - strip-ansi: 7.2.0 - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.2.0 - wsl-utils@0.3.1: + wsl-utils@1.0.0: dependencies: is-wsl: 3.1.1 powershell-utils: 0.1.0 y18n@5.0.8: {} - yallist@5.0.0: - optional: true + yallist@5.0.0: {} - yaml@2.9.0: + yaml@2.9.1: optional: true yargs-parser@22.0.0: {} - yargs@18.0.0: + yargs@18.2.0: dependencies: cliui: 9.0.1 escalade: 3.2.0 get-caller-file: 2.0.5 - string-width: 7.2.0 + string-width: 8.2.2 y18n: 5.0.8 yargs-parser: 22.0.0 yocto-queue@0.1.0: {} - yoctocolors@2.1.2: {} + yoctocolors@2.2.0: {} + + yuku-ast@0.10.2: + dependencies: + '@yuku-toolchain/types': 0.10.2 + + yuku-codegen@0.10.2: + dependencies: + '@yuku-toolchain/types': 0.10.2 + optionalDependencies: + '@yuku-codegen/binding-android-arm64': 0.10.2 + '@yuku-codegen/binding-darwin-arm64': 0.10.2 + '@yuku-codegen/binding-darwin-x64': 0.10.2 + '@yuku-codegen/binding-freebsd-x64': 0.10.2 + '@yuku-codegen/binding-linux-arm-gnu': 0.10.2 + '@yuku-codegen/binding-linux-arm-musl': 0.10.2 + '@yuku-codegen/binding-linux-arm64-gnu': 0.10.2 + '@yuku-codegen/binding-linux-arm64-musl': 0.10.2 + '@yuku-codegen/binding-linux-x64-gnu': 0.10.2 + '@yuku-codegen/binding-linux-x64-musl': 0.10.2 + '@yuku-codegen/binding-win32-arm64': 0.10.2 + '@yuku-codegen/binding-win32-x64': 0.10.2 + + yuku-parser@0.10.2: + dependencies: + '@yuku-toolchain/types': 0.10.2 + yuku-ast: 0.10.2 + optionalDependencies: + '@yuku-parser/binding-android-arm64': 0.10.2 + '@yuku-parser/binding-darwin-arm64': 0.10.2 + '@yuku-parser/binding-darwin-x64': 0.10.2 + '@yuku-parser/binding-freebsd-x64': 0.10.2 + '@yuku-parser/binding-linux-arm-gnu': 0.10.2 + '@yuku-parser/binding-linux-arm-musl': 0.10.2 + '@yuku-parser/binding-linux-arm64-gnu': 0.10.2 + '@yuku-parser/binding-linux-arm64-musl': 0.10.2 + '@yuku-parser/binding-linux-x64-gnu': 0.10.2 + '@yuku-parser/binding-linux-x64-musl': 0.10.2 + '@yuku-parser/binding-win32-arm64': 0.10.2 + '@yuku-parser/binding-win32-x64': 0.10.2 + + zod@4.6.5: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2cd98ab..cecfa10 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,53 +1,64 @@ +packages: + - . + - libs/* + allowBuilds: bun: true catalogs: build: - tsdown: ^0.22.3 + cpx2: ^9.0.0 + tsdown: ^0.23.0 + turbo: ^2.11.3 typescript: ^6.0.3 ci: - '@commitlint/cli': ^21.2.0 - '@commitlint/config-conventional': ^21.2.0 + '@commitlint/cli': ^21.2.3 + '@commitlint/config-conventional': ^21.2.3 '@favware/cliff-jumper': ^6.1.0 '@nanoforge-dev/actions': ^2.1.5 husky: ^9.1.7 - lint-staged: ^17.0.8 + lint-staged: ^17.5.1 cli: - '@inquirer/prompts': ^8.5.2 + '@inquirer/prompts': ^8.7.2 '@types/inquirer': ^9.0.10 - ansis: ^4.3.1 + ansis: ^4.4.0 commander: ^15.0.0 node-emoji: ^2.2.0 ora: ^9.4.1 core: '@nanoforge-dev/editor': ^0.1.4 - '@types/node': ^26.1.0 - bun: ^1.3.14 + '@types/bun': ^1.3.14 + '@types/node': ^26.6.2 + bun: ^1.4.2 libs: chokidar: ^5.0.0 class-transformer: ^0.5.1 class-validator: ^0.15.1 - dotenv: ^17.4.2 - open: ^11.0.0 - rc9: ^3.0.1 + dotenv: ^18.0.3 + open: ^11.0.4 + rc9: ^3.1.0 reflect-metadata: ^0.2.2 + unrun: ^0.3.0 + zod: ^4.5.4 lint: '@nanoforge-dev/utils-eslint-config': ^1.0.2 '@nanoforge-dev/utils-prettier-config': ^1.0.2 '@trivago/prettier-plugin-sort-imports': ^6.0.2 - eslint: ^10.6.0 - prettier: ^3.9.4 + eslint: ^10.11.0 + prettier: ^3.9.9 loader: - '@nanoforge-dev/loader-client': ^1.4.0 - '@nanoforge-dev/loader-server': ^1.4.0 + '@nanoforge-dev/loader-client': ^1.5.1 + '@nanoforge-dev/loader-server': ^1.5.1 schematics: - '@angular-devkit/schematics': ^22.0.5 - '@angular-devkit/schematics-cli': ^22.0.5 - '@nanoforge-dev/schematics': ^2.2.1 - tests: - '@vitest/coverage-v8': ^4.1.10 - vitest: ^4.1.10 + '@angular-devkit/core': ^22.0.4 + '@angular-devkit/schematics': ^22.1.8 + '@angular-devkit/schematics-cli': ^22.1.8 + rxjs: ^7.8.2 + test: + '@vitest/coverage-v8': ^5.0.1 + vitest: ^5.0.1 minimumReleaseAgeExclude: - - '@nanoforge-dev/editor@0.1.4' - - '@nanoforge-dev/schematics@2.2.1' + - '@nanoforge-dev/loader-client@1.5.1' + - '@nanoforge-dev/loader-server@1.5.1' + - '@nanoforge-dev/loader-website@1.5.1' diff --git a/src/action/actions/build.action.ts b/src/action/actions/build.action.ts index df8ae2f..f3c118f 100644 --- a/src/action/actions/build.action.ts +++ b/src/action/actions/build.action.ts @@ -1,7 +1,13 @@ +import { defaultClientConfig, defaultServerConfig } from "@nanoforge-dev/config"; import { watch } from "chokidar"; -import { dirname, join } from "node:path"; +import { dirname, join, relative } from "node:path"; -import { type Config } from "@lib/config"; +import { + type ResolvedProject, + type WorkspaceConfig, + parseWorkspaceConfig, + resolveProjects, +} from "@lib/config"; import { type Input, getDirectoryInput, @@ -16,14 +22,13 @@ import { copyFiles, resetFolder } from "@utils/files"; import { getCwd } from "@utils/path"; import { runSafe } from "@utils/run-safe"; -import { getConfig } from "~/action/common/config"; - import { AbstractAction, type HandleResult } from "../abstract.action"; interface BuildTarget { name: string; + directory: string; entry: string; - static: string; + static?: string; output: string; platform: "browser" | "node"; } @@ -35,12 +40,12 @@ export class BuildAction extends AbstractAction { public async handle(_args: Input, options: Input): Promise { const directory = getDirectoryInput(options); - const config = await getConfig(options, directory); + const workspaceConfig = await parseWorkspaceConfig(directory); const isEditor = getEditorInput(options); const isWatch = getWatchInput(options); - const targets = this.resolveTargets(config, options, isEditor); - const results = await this.buildAll(targets, directory, isWatch); + const targets = this.resolveTargets(workspaceConfig, directory, options, isEditor); + const results = await this.buildAll(targets, isWatch); if (isWatch) { return this.enterWatchMode(); @@ -49,85 +54,77 @@ export class BuildAction extends AbstractAction { return { success: results.every(Boolean) }; } - private resolveTargets(config: Config, options: Input, isEditor: boolean): BuildTarget[] { - const targets: BuildTarget[] = []; - - if (config.client.enable) - targets.push( - this.createTarget( - "Client", - - "browser", - getStringInputWithDefault( - options, - "clientEntry", - !isEditor ? config.client.build.entry : config.client.editor.entry, - ), - getStringInputWithDefault(options, "clientStaticDir", config.client.build.staticDir), - getStringInputWithDefault(options, "clientOutDir", config.client.outDir), - ), - ); - if (config.server.enable) - targets.push( - this.createTarget( - "Server", - "node", - getStringInputWithDefault( - options, - "serverEntry", - !isEditor ? config.server.build.entry : config.server.editor.entry, - ), - getStringInputWithDefault(options, "serverStaticDir", config.server.build.staticDir), - getStringInputWithDefault(options, "serverOutDir", config.server.outDir), - ), - ); - - return targets; + private resolveTargets( + workspaceConfig: WorkspaceConfig, + baseDirectory: string, + options: Input, + isEditor: boolean, + ): BuildTarget[] { + const projects = resolveProjects(workspaceConfig, baseDirectory); + + return projects.map((project) => + this.createTarget(project, baseDirectory, options, isEditor, projects.length > 1), + ); } private createTarget( - name: string, - platform: "browser" | "node", - entryFile: string, - staticDir: string, - outDir: string, + project: ResolvedProject, + baseDirectory: string, + options: Input, + isEditor: boolean, + disambiguate: boolean, ): BuildTarget { + const isClient = project.config.type === "client"; + const label = isClient ? "Client" : "Server"; + const relativePath = relative(baseDirectory, project.directory); + const name = disambiguate && relativePath !== "" ? `${label} (${relativePath})` : label; + const defaults = isClient ? defaultClientConfig : defaultServerConfig; + + const defaultEntry = isEditor + ? (project.config.editor?.entryFile ?? defaults.editor.entryFile) + : (project.config.entryFile ?? defaults.entryFile); + return { name, - entry: entryFile, - static: staticDir, - output: outDir, - platform, + directory: project.directory, + entry: getStringInputWithDefault( + options, + isClient ? "clientEntry" : "serverEntry", + defaultEntry, + ), + static: getStringInputWithDefault( + options, + isClient ? "clientStaticDir" : "serverStaticDir", + project.config.dir?.assets ?? defaults.dir.assets, + ), + output: getStringInputWithDefault( + options, + isClient ? "clientOutDir" : "serverOutDir", + project.config.out?.dir ?? defaults.out.dir, + ), + platform: isClient ? "browser" : "node", }; } - private async buildAll( - targets: BuildTarget[], - directory: string, - isWatch: boolean, - ): Promise { + private async buildAll(targets: BuildTarget[], isWatch: boolean): Promise { const results: boolean[] = []; for (const target of targets) { - const result = await this.buildTarget(target, directory, isWatch); + const result = await this.buildTarget(target, isWatch); results.push(result); } return results; } - private async buildTarget( - target: BuildTarget, - directory: string, - isWatch: boolean, - ): Promise { + private async buildTarget(target: BuildTarget, isWatch: boolean): Promise { const packageManager = PackageManagerFactory.create(PackageManagerName.LOCAL_BUN); const executeBuild = (rebuild = false) => runSafe(() => { - this.resetOut(target.output, directory); - this.copyFiles(target, directory); + this.resetOut(target.output, target.directory); + this.copyFiles(target, target.directory); return packageManager.build( target.name, - directory, + target.directory, target.entry, target.output, ["--asset-naming", "[name].[ext]", "--target", target.platform], @@ -136,7 +133,7 @@ export class BuildAction extends AbstractAction { }, false); if (isWatch) { - this.watchDirectory(directory, target.entry, () => executeBuild(true)); + this.watchDirectory(target.directory, target.entry, () => executeBuild(true)); } const result = await executeBuild(); @@ -160,6 +157,7 @@ export class BuildAction extends AbstractAction { } private copyFiles(target: BuildTarget, directory: string): void { + if (!target.static) return; const from = getCwd(join(directory, target.static)); const to = getCwd(join(directory, target.output)); copyFiles(from, to); diff --git a/src/action/actions/create.action.ts b/src/action/actions/create.action.ts index a2b6605..9c2a05f 100644 --- a/src/action/actions/create.action.ts +++ b/src/action/actions/create.action.ts @@ -1,18 +1,20 @@ +import { defaultClientConfig, defaultServerConfig } from "@nanoforge-dev/config"; + +import { parseWorkspaceConfig } from "@lib/config"; import { type Input, getCreateNameInputOrAsk, getCreateTypeInput, getDirectoryInput, getPathInputWithDefault, - getServerInput, } from "@lib/input"; import { Collection, CollectionFactory } from "@lib/schematics"; import { Messages } from "@lib/ui"; +import { CLIError } from "@utils/errors"; import { capitalize } from "@utils/formatting"; import { AbstractAction, type HandleResult } from "../abstract.action"; -import { getConfig } from "../common/config"; import { executeSchematic } from "../common/schematics"; interface CreateValues { @@ -29,22 +31,33 @@ export class CreateAction extends AbstractAction { public async handle(args: Input, options: Input): Promise { const directory = getDirectoryInput(options); - const config = await getConfig(options, directory, true); + const workspaceConfig = await parseWorkspaceConfig(directory); + + if (workspaceConfig.type === "workspace") { + throw new CLIError( + `Cannot run 'create' at a workspace root ('${directory}').`, + "Run it from within a specific client/server project directory instead (-d ).", + ); + } + + const { config } = workspaceConfig; + const isClient = config.type === "client"; + const defaults = isClient ? defaultClientConfig : defaultServerConfig; const type = getCreateTypeInput(args); const name = await getCreateNameInputOrAsk(options); - const isServer = getServerInput(options); const path = getPathInputWithDefault( options, - config[isServer ? "server" : "client"].dirs[type === "component" ? "components" : "systems"], + config.dir?.[type === "component" ? "components" : "systems"] ?? + defaults.dir[type === "component" ? "components" : "systems"], ); await this.generateElement(directory, type, { name, directory: path, - part: isServer ? "server" : "client", - language: config.language, + part: config.type, + language: config.language ?? defaults.language, }); return {}; diff --git a/src/action/actions/dev.action.ts b/src/action/actions/dev.action.ts index e7b88fc..2191b04 100644 --- a/src/action/actions/dev.action.ts +++ b/src/action/actions/dev.action.ts @@ -1,4 +1,4 @@ -import { type Input, getDevGenerateInput, getDirectoryInput, getEditorInput } from "@lib/input"; +import { type Input, getDirectoryInput, getEditorInput } from "@lib/input"; import { PackageManagerFactory } from "@lib/package-manager"; import { Messages } from "@lib/ui"; @@ -13,23 +13,18 @@ export class DevAction extends AbstractAction { public async handle(_args: Input, options: Input): Promise { const directory = getDirectoryInput(options); - const generate = getDevGenerateInput(options); const editor = getEditorInput(options); - const tasks = this.buildTaskList(directory, generate, editor); + const tasks = this.buildTaskList(directory, editor); await Promise.all(tasks); return { keepAlive: true }; } - private buildTaskList(directory: string, generate: boolean, editor: boolean): Promise[] { + private buildTaskList(directory: string, editor: boolean): Promise[] { const tasks: Promise[] = []; const extraFlags = editor ? ["--editor"] : []; - if (generate) { - tasks.push(this.runSubCommand("generate", directory, { silent: true, extraFlags })); - } - tasks.push(this.runSubCommand("build", directory, { silent: true, extraFlags })); tasks.push(this.runSubCommand("start", directory, { silent: false })); diff --git a/src/action/actions/generate.action.ts b/src/action/actions/generate.action.ts deleted file mode 100644 index 06389cf..0000000 --- a/src/action/actions/generate.action.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { join } from "node:path"; - -import { type Config } from "@lib/config"; -import { type Input, getDirectoryInput, getEditorInput, getWatchInput } from "@lib/input"; -import { Collection, CollectionFactory } from "@lib/schematics"; -import { Messages } from "@lib/ui"; - -import { getCwd } from "@utils/path"; - -import { AbstractAction, type HandleResult } from "../abstract.action"; -import { getConfig } from "../common/config"; -import { executeSchematic } from "../common/schematics"; - -interface GenerateValues { - directory: string; - language: string; - initFunctions: boolean; -} - -export class GenerateAction extends AbstractAction { - protected startMessage = Messages.GENERATE_START; - protected successMessage = Messages.GENERATE_SUCCESS; - protected failureMessage = Messages.GENERATE_FAILED; - - public async handle(_args: Input, options: Input): Promise { - const directory = getDirectoryInput(options); - const config = await getConfig(options, directory); - const isEditor = getEditorInput(options); - const isWatch = getWatchInput(options); - - await this.generateParts(config, directory, isEditor, isWatch); - - if (isWatch) { - return this.enterWatchMode(); - } - - return {}; - } - - private extractValues(config: Config): GenerateValues { - return { - directory: ".", - language: config.language, - initFunctions: config.initFunctions, - }; - } - - private async generateParts( - config: Config, - directory: string, - isEditor: boolean, - watch: boolean, - ): Promise { - const collection = CollectionFactory.create(Collection.NANOFORGE, directory); - const values = this.extractValues(config); - - if (config.client.enable) - await executeSchematic( - "Client main file", - collection, - "part-main", - { - ...values, - part: "client", - outFile: !isEditor ? config.client.build.entry : config.client.editor.entry, - saveFile: config.client.editor.save, - editor: isEditor, - }, - watch ? this.watchPath(directory, values.directory, config.client.editor.save) : undefined, - ); - - if (config.server.enable) - await executeSchematic( - "Server main file", - collection, - "part-main", - { - ...values, - part: "server", - outFile: !isEditor ? config.server.build.entry : config.server.editor.entry, - saveFile: config.server.editor.save, - editor: isEditor, - }, - this.watchPath(directory, values.directory, config.server.editor.save), - ); - } - - private watchPath(directory: string, subDir: string, saveFile: string): string { - return join(getCwd(directory), subDir, saveFile); - } - - private enterWatchMode(): HandleResult { - console.info(); - console.info(Messages.GENERATE_WATCH_START); - console.info(); - return { keepAlive: true }; - } -} diff --git a/src/action/actions/new.action.ts b/src/action/actions/new.action.ts index 6220c46..bab6fb8 100644 --- a/src/action/actions/new.action.ts +++ b/src/action/actions/new.action.ts @@ -6,7 +6,6 @@ import { getDirectoryInput, getEditorInput, getNewDockerOrAsk, - getNewInitFunctionsWithDefault, getNewLanguageInputOrAsk, getNewLintInput, getNewNameInputOrAsk, @@ -31,8 +30,7 @@ interface NewValues { packageManager: string; language: string; strict: boolean; - server: boolean; - initFunctions: boolean; + multiplayer: boolean; skipInstall: boolean; docker: boolean; lint: boolean; @@ -71,8 +69,7 @@ export class NewAction extends AbstractAction { packageManager: await getNewPackageManagerInputOrAsk(inputs), language: await getNewLanguageInputOrAsk(inputs), strict: await getNewStrictOrAsk(inputs), - server: await getNewServerOrAsk(inputs), - initFunctions: getNewInitFunctionsWithDefault(inputs), + multiplayer: await getNewServerOrAsk(inputs), skipInstall: await getNewSkipInstallOrAsk(inputs), docker: await getNewDockerOrAsk(inputs), lint: getNewLintInput(inputs), @@ -94,91 +91,63 @@ export class NewAction extends AbstractAction { console.info(Messages.NEW_GENERATION_START); console.info(); - await this.generateApplication(collection, values); - await this.generateConfiguration(collection, values); - await this.generateClientParts(collection, values); - if (values.docker) await this.generateDocker(collection, values); - - if (values.server) { - await this.generateServerParts(collection, values); + if (values.multiplayer) { + await this.generateWorkspace(collection, values); + await this.generateClientProject(collection, values); + await this.generateServerProject(collection, values); + } else { + await this.generateClientProject(collection, values); } } - private generateApplication( + private generateWorkspace( collection: ReturnType, values: NewValues, ) { - return executeSchematic("Application", collection, "application", { + return executeSchematic("Workspace", collection, "workspace", { name: values.name, directory: values.directory, - packageManager: values.packageManager, language: values.language, strict: values.strict, - server: values.server, - lint: values.lint, - editor: values.editor, - }); - } - - private generateConfiguration( - collection: ReturnType, - values: NewValues, - ) { - return executeSchematic("Configuration", collection, "configuration", { - name: values.name, - directory: values.directory, - server: values.server, - language: values.language, - initFunctions: values.initFunctions, - }); - } - - private async generateClientParts( - collection: ReturnType, - values: NewValues, - ) { - const partOptions = this.partOptions(values, "client"); - - await executeSchematic("Client base", collection, "part-base", { - ...partOptions, - server: values.server, - }); - await executeSchematic("Client main file", collection, "part-main", { - ...partOptions, + packageManager: values.packageManager, + docker: values.docker, }); } - private async generateServerParts( + private generateClientProject( collection: ReturnType, values: NewValues, ) { - const partOptions = this.partOptions(values, "server"); - - await executeSchematic("Server base", collection, "part-base", { - ...partOptions, - server: values.server, - }); - await executeSchematic("Server main file", collection, "part-main", { - ...partOptions, + return executeSchematic("Client project", collection, "project", { + ...this.projectOptions(values, "client"), + hasServer: values.multiplayer, }); } - private async generateDocker( + private generateServerProject( collection: ReturnType, values: NewValues, ) { - await executeSchematic("Docker", collection, "docker", { - directory: values.directory, - packageManager: values.packageManager, - }); + return executeSchematic( + "Server project", + collection, + "project", + this.projectOptions(values, "server"), + ); } - private partOptions(values: NewValues, part: "client" | "server") { + private projectOptions(values: NewValues, part: "client" | "server") { return { part, - directory: values.directory, + workspaceName: values.multiplayer ? values.name : undefined, + name: values.multiplayer ? part : values.name, + directory: values.multiplayer ? join(values.directory, "apps", part) : values.directory, language: values.language, - initFunctions: values.initFunctions, + strict: values.strict, + packageManager: values.packageManager, + workspace: values.multiplayer, + docker: values.docker, + editor: values.editor, }; } diff --git a/src/action/actions/start.action.ts b/src/action/actions/start.action.ts index 2198e37..8aca366 100644 --- a/src/action/actions/start.action.ts +++ b/src/action/actions/start.action.ts @@ -1,7 +1,13 @@ +import { type ClientConfig, defaultClientConfig, defaultServerConfig } from "@nanoforge-dev/config"; import dotenv from "dotenv"; import { join, resolve } from "path"; -import { type Config } from "@lib/config"; +import { + type ResolvedProject, + type WorkspaceConfig, + parseWorkspaceConfig, + resolveProjects, +} from "@lib/config"; import { type Input, getDirectoryInput, @@ -9,18 +15,16 @@ import { getStringInputWithDefault, getWatchInput, } from "@lib/input"; -import { PackageManagerFactory } from "@lib/package-manager"; +import { type PackageManager, PackageManagerFactory } from "@lib/package-manager"; import { Messages } from "@lib/ui"; import { CLIError } from "@utils/errors"; import { getCwd, getModulePath } from "@utils/path"; import { runSafe } from "@utils/run-safe"; -import { getConfig } from "~/action/common/config"; - import { AbstractAction, type HandleResult } from "../abstract.action"; -interface SSLOptions { +interface TLSOptions { cert: string; key: string; } @@ -30,6 +34,27 @@ interface FullEnv { server: Record; } +interface ClientStartTarget { + directory: string; + outDir: string; + platform: "client"; + port: string; + tls?: TLSOptions; +} + +interface ServerStartTarget { + directory: string; + outDir: string; + platform: "server"; +} + +type StartTarget = ClientStartTarget | ServerStartTarget; + +const isClientTarget = (target: StartTarget): target is ClientStartTarget => + target.platform === "client"; +const isServerTarget = (target: StartTarget): target is ServerStartTarget => + target.platform === "server"; + export class StartAction extends AbstractAction { protected startMessage = Messages.START_START; protected successMessage = Messages.START_SUCCESS; @@ -37,155 +62,170 @@ export class StartAction extends AbstractAction { public async handle(_args: Input, options: Input): Promise { const directory = getDirectoryInput(options); - const config = await getConfig(options, directory); - const clientDir = getStringInputWithDefault(options, "clientDir", config.client.outDir); - const serverDir = getStringInputWithDefault(options, "serverDir", config.server.outDir); + const workspaceConfig = await parseWorkspaceConfig(directory); const watch = getWatchInput(options); - const port = getStringInputWithDefault(options, "port", config.client.port); - const ssl = this.resolveSSL(options, config); - - const tasks = this.buildStartTasks(config, directory, { - clientDir, - serverDir, - watch, - port, - ssl, - }); + + const targets = this.resolveTargets(workspaceConfig, directory, options); + const serverTargets = targets.filter(isServerTarget); + const clientTargets = targets.filter(isClientTarget); + + if (targets.length === 0) { + throw new CLIError( + `No client or server project found at '${directory}'.`, + "Check your nanoforge.config's 'packages' patterns, or run 'nf new' to scaffold one.", + ); + } + + const env = this.parseEnv(directory); + const packageManager = await PackageManagerFactory.find(directory); + const tasks = this.buildStartTasks(packageManager, serverTargets, clientTargets, watch, env); await Promise.all(tasks); return { keepAlive: true }; } - private resolveSSL(options: Input, config: Config): SSLOptions | undefined { + private resolveTLS(options: Input, config: ClientConfig): TLSOptions | undefined { const cliCert = getStringInput(options, "cert"); const cliKey = getStringInput(options, "key"); - const isSslRequested = Boolean(cliCert || cliKey || config.ssl?.enable); + const tls = config.tls; + const configTls = tls && tls.enable ? tls : undefined; + const isTlsRequested = Boolean(cliCert || cliKey || configTls); - if (!isSslRequested) return undefined; + if (!isTlsRequested) return undefined; - const cert = cliCert ? cliCert : config.ssl?.cert; - const key = cliKey ? cliKey : config.ssl?.key; + const cert = cliCert ?? configTls?.cert; + const key = cliKey ?? configTls?.key; if (!cert) { throw new CLIError( - "No certificate found for SSL.", - "Please provide a certificate path with --cert or configure 'ssl.cert' in your nanoforge.config.json.", + "No certificate found for TLS.", + "Please provide a certificate path with --cert or configure 'tls.cert' in your nanoforge.config.ts", ); } if (!key) { throw new CLIError( - "No key found for SSL.", - "Please provide a key path with --key or configure 'ssl.key' in your nanoforge.config.json.", + "No key found for TLS.", + "Please provide a key path with --key or configure 'tls.key' in your nanoforge.config.ts", ); } + return { cert, key }; + } + + private resolveTargets( + workspaceConfig: WorkspaceConfig, + baseDirectory: string, + options: Input, + ): StartTarget[] { + const projects = resolveProjects(workspaceConfig, baseDirectory); + return projects.map((project) => this.createTarget(project, options)); + } + + private createTarget(project: ResolvedProject, options: Input): StartTarget { + if (project.config.type === "server") { + return { + directory: project.directory, + outDir: getStringInputWithDefault( + options, + "serverDir", + project.config.out?.dir ?? defaultServerConfig.out.dir, + ), + platform: "server", + }; + } + return { - cert, - key, + directory: project.directory, + outDir: getStringInputWithDefault( + options, + "clientDir", + project.config.out?.dir ?? defaultClientConfig.out.dir, + ), + platform: "client", + port: getStringInputWithDefault( + options, + "port", + project.config.port ?? defaultClientConfig.port, + ), + tls: this.resolveTLS(options, project.config), }; } private buildStartTasks( - config: Config, - directory: string, - options: { - clientDir: string; - serverDir: string; - watch: boolean; - port: string; - ssl?: SSLOptions; - }, + packageManager: PackageManager, + serverTargets: ServerStartTarget[], + clientTargets: ClientStartTarget[], + watch: boolean, + env: FullEnv, ): Promise[] { - const env = this.parseEnv(directory); const tasks: Promise[] = []; - const { clientDir, serverDir, watch, port, ssl } = options; - if (config.server.enable) - tasks.push(this.startServer(directory, config, { serverDir, watch }, env)); + for (const target of serverTargets) { + tasks.push(this.startServer(packageManager, target, watch, env)); + } - if (config.client.enable) - tasks.push( - this.startClient(directory, config, { clientDir, serverDir, watch, port, ssl }, env), - ); + for (const target of clientTargets) { + tasks.push(this.startClient(packageManager, target, { watch, serverTargets }, env)); + } return tasks; } private async startClient( - directory: string, - config: Config, - options: { - clientDir: string; - serverDir: string; - watch: boolean; - port: string; - ssl?: SSLOptions; - }, + packageManager: PackageManager, + target: ClientStartTarget, + options: { watch: boolean; serverTargets: ServerStartTarget[] }, env: FullEnv, ): Promise { const loaderPath = getModulePath("@nanoforge-dev/loader-client/package.json", true); - const params = this.buildClientParams(directory, config, options); - await this.runLoader("Client", loaderPath, params, env.client); + const params = this.buildClientParams(target, options); + await this.runLoader(packageManager, "Client", loaderPath, params, env.client); } private async startServer( - directory: string, - config: Config, - options: { serverDir: string; watch: boolean }, + packageManager: PackageManager, + target: ServerStartTarget, + watch: boolean, env: FullEnv, ): Promise { const loaderPath = getModulePath("@nanoforge-dev/loader-server/package.json", true); - const params = this.buildServerParams(directory, config, options); - await this.runLoader("Server", loaderPath, params, env.server); + const params = this.buildServerParams(target, watch); + await this.runLoader(packageManager, "Server", loaderPath, params, env.server); } private buildClientParams( - directory: string, - config: Config, - options: { - clientDir: string; - serverDir: string; - watch: boolean; - port: string; - ssl?: SSLOptions; - }, + target: ClientStartTarget, + options: { watch: boolean; serverTargets: ServerStartTarget[] }, ): string[] { const params: Record = { - "-d": getCwd(join(directory, options.clientDir)), - "-p": options.port, + "-d": getCwd(join(target.directory, target.outDir)), + "-p": target.port, }; - if (options.watch) params["--watch"] = true; if (options.watch) { params["--watch"] = true; - if (config.server.enable) { - params["--watch-server-dir"] = getCwd(join(directory, options.serverDir)); + const [server] = options.serverTargets; + if (server && options.serverTargets.length === 1) { + params["--watch-server-dir"] = getCwd(join(server.directory, server.outDir)); } } - if (options.ssl) { - params["--cert"] = options.ssl.cert; - params["--key"] = options.ssl.key; + if (target.tls) { + params["--cert"] = target.tls.cert; + params["--key"] = target.tls.key; } return this.buildParams(params); } - private buildServerParams( - directory: string, - _config: Config, - options: { - serverDir: string; - watch: boolean; - }, - ): string[] { + private buildServerParams(target: ServerStartTarget, watch: boolean): string[] { const params: Record = { - "-d": getCwd(join(directory, options.serverDir)), + "-d": getCwd(join(target.directory, target.outDir)), }; - if (options.watch) params["--watch"] = true; + if (watch) params["--watch"] = true; return this.buildParams(params); } @@ -227,13 +267,13 @@ export class StartAction extends AbstractAction { } private async runLoader( + packageManager: PackageManager, name: string, directory: string, params: string[], env: Record, ): Promise { await runSafe(async () => { - const packageManager = await PackageManagerFactory.find(directory); await packageManager.run(name, directory, "start", params, env, [], true); }); } diff --git a/src/action/common/config.ts b/src/action/common/config.ts deleted file mode 100644 index f7e879e..0000000 --- a/src/action/common/config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type Config, loadConfig } from "@lib/config"; -import { type Input, getConfigInput } from "@lib/input"; - -export const getConfig = (inputs: Input, dir: string, noThrow?: boolean): Promise => { - return loadConfig(dir, getConfigInput(inputs), noThrow); -}; diff --git a/src/action/index.ts b/src/action/index.ts index 0d06368..ad55b22 100644 --- a/src/action/index.ts +++ b/src/action/index.ts @@ -2,7 +2,6 @@ export * from "./actions/build.action"; export * from "./actions/create.action"; export * from "./actions/dev.action"; export * from "./actions/editor.action"; -export * from "./actions/generate.action"; export * from "./actions/install.action"; export * from "./actions/login.action"; export * from "./actions/logout.action"; diff --git a/src/command/command.loader.ts b/src/command/command.loader.ts index e06c1d5..51c4192 100644 --- a/src/command/command.loader.ts +++ b/src/command/command.loader.ts @@ -8,7 +8,6 @@ import { CreateAction, DevAction, EditorAction, - GenerateAction, InstallAction, LoginAction, LogoutAction, @@ -22,7 +21,6 @@ import { BuildCommand } from "./commands/build.command"; import { CreateCommand } from "./commands/create.command"; import { DevCommand } from "./commands/dev.command"; import { EditorCommand } from "./commands/editor.command"; -import { GenerateCommand } from "./commands/generate.command"; import { InstallCommand } from "./commands/install.command"; import { LoginCommand } from "./commands/login.command"; import { LogoutCommand } from "./commands/logout.command"; @@ -39,7 +37,6 @@ export class CommandLoader { new StartCommand(new StartAction()).load(program); new DevCommand(new DevAction()).load(program); new EditorCommand(new EditorAction()).load(program); - new GenerateCommand(new GenerateAction()).load(program); new CreateCommand(new CreateAction()).load(program); new LoginCommand(new LoginAction()).load(program); new LogoutCommand(new LogoutAction()).load(program); diff --git a/src/command/commands/build.command.ts b/src/command/commands/build.command.ts index 61e6472..8619ef1 100644 --- a/src/command/commands/build.command.ts +++ b/src/command/commands/build.command.ts @@ -1,12 +1,9 @@ import { type Command } from "commander"; -import { CONFIG_FILE_NAME } from "@lib/constants"; - import { AbstractCommand } from "../abstract.command"; interface BuildOptions { directory?: string; - config?: string; clientEntry?: string; serverEntry?: string; clientStaticDir?: string; @@ -23,7 +20,6 @@ export class BuildCommand extends AbstractCommand { .command("build") .description("build your game") .option("-d, --directory ", "specify the working directory of the command") - .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME) .option("--client-entry ", "specify the entry file of the client") .option("--server-entry ", "specify the entry file of the server") .option("--client-static-dir ", "specify the static directory of the client") @@ -35,7 +31,6 @@ export class BuildCommand extends AbstractCommand { .action(async (rawOptions: BuildOptions) => { const options = AbstractCommand.mapToInput({ directory: rawOptions.directory, - config: rawOptions.config, clientEntry: rawOptions.clientEntry, serverEntry: rawOptions.serverEntry, clientStaticDir: rawOptions.clientStaticDir, diff --git a/src/command/commands/create.command.ts b/src/command/commands/create.command.ts index 7f91988..30d0950 100644 --- a/src/command/commands/create.command.ts +++ b/src/command/commands/create.command.ts @@ -1,14 +1,10 @@ import { type Command } from "commander"; -import { CONFIG_FILE_NAME } from "@lib/constants"; - import { AbstractCommand } from "../abstract.command"; interface CreateOptions { directory?: string; - config: string; name?: string; - server: boolean; path?: string; } @@ -18,13 +14,7 @@ export class CreateCommand extends AbstractCommand { .command("create ") .description("create nanoforge components or systems") .option("-d, --directory ", "specify the working directory of the command") - .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME) .option("-n, --name ", "name of the component/system") - .option( - "-s, --server", - "install components/systems on server (default install on client)", - false, - ) .option( "-p, --path ", "path to the component/system folder (default: /)", @@ -36,9 +26,7 @@ export class CreateCommand extends AbstractCommand { const options = AbstractCommand.mapToInput({ directory: rawOptions.directory, - config: rawOptions.config, name: rawOptions.name, - server: rawOptions.server, path: rawOptions.path, }); diff --git a/src/command/commands/dev.command.ts b/src/command/commands/dev.command.ts index a985e9f..b239d7e 100644 --- a/src/command/commands/dev.command.ts +++ b/src/command/commands/dev.command.ts @@ -7,7 +7,6 @@ import { AbstractCommand } from "../abstract.command"; interface DevOptions { directory?: string; config?: string; - generate?: boolean; editor?: boolean; } @@ -18,13 +17,11 @@ export class DevCommand extends AbstractCommand { .description("run your game in dev mode") .option("-d, --directory ", "specify the working directory of the command") .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME) - .option("--generate", "generate app from config", false) .option("-e, --editor", "run the editor", false) .action(async (rawOptions: DevOptions) => { const options = AbstractCommand.mapToInput({ directory: rawOptions.directory, config: rawOptions.config, - generate: rawOptions.generate, editor: rawOptions.editor, }); diff --git a/src/command/commands/generate.command.ts b/src/command/commands/generate.command.ts deleted file mode 100644 index 11203f7..0000000 --- a/src/command/commands/generate.command.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type Command } from "commander"; - -import { CONFIG_FILE_NAME } from "@lib/constants"; - -import { AbstractCommand } from "../abstract.command"; - -interface GenerateOptions { - directory?: string; - config?: string; - editor?: boolean; - watch?: boolean; -} - -export class GenerateCommand extends AbstractCommand { - public load(program: Command) { - program - .command("generate") - .description("generate nanoforge files from config") - .option("-d, --directory ", "specify the working directory of the command") - .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME) - .option("--editor", "specify if the project must generate editor main file") - .option("--watch", "generate app in watching mode", false) - .action(async (rawOptions: GenerateOptions) => { - const options = AbstractCommand.mapToInput({ - directory: rawOptions.directory, - config: rawOptions.config, - editor: rawOptions.editor, - watch: rawOptions.watch, - }); - - await this.action.run(new Map(), options); - }); - } -} diff --git a/src/command/commands/start.command.ts b/src/command/commands/start.command.ts index 4299acc..036afda 100644 --- a/src/command/commands/start.command.ts +++ b/src/command/commands/start.command.ts @@ -1,12 +1,9 @@ import { type Command } from "commander"; -import { CONFIG_FILE_NAME } from "@lib/constants"; - import { AbstractCommand } from "../abstract.command"; interface StartOptions { directory?: string; - config?: string; port?: string; clientDir?: string; serverDir?: string; @@ -21,17 +18,15 @@ export class StartCommand extends AbstractCommand { .command("start") .description("start your game") .option("-d, --directory ", "specify the working directory of the command") - .option("-c, --config ", "path to the config file", CONFIG_FILE_NAME) .option("-p, --port ", "specify the port of the loader (the website to load the game)") - .option("--client-dir ", "specify the directory of the client") - .option("--server-dir ", "specify the directory of the server") + .option("--client-dir ", "specify the output directory of the client") + .option("--server-dir ", "specify the output directory of the server") .option("--watch", "run app in watching mode", false) - .option("--cert ", "path to the SSL certificate for HTTPS") - .option("--key ", "path to the SSL key for HTTPS") + .option("--cert ", "path to the TLS certificate for HTTPS") + .option("--key ", "path to the TLS key for HTTPS") .action(async (rawOptions: StartOptions) => { const options = AbstractCommand.mapToInput({ directory: rawOptions.directory, - config: rawOptions.config, port: rawOptions.port, clientDir: rawOptions.clientDir, serverDir: rawOptions.serverDir, diff --git a/src/lib/config/config-defaults.ts b/src/lib/config/config-defaults.ts deleted file mode 100644 index fe84202..0000000 --- a/src/lib/config/config-defaults.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { type Config } from "./config.type"; - -export const CONFIG_DEFAULTS: Config = { - name: "nanoforge-app", - language: "ts", - initFunctions: false, - ssl: { - enable: false, - cert: "", - key: "", - }, - client: { - enable: true, - port: "3000", - outDir: ".nanoforge/client", - build: { - entry: "client/main.ts", - staticDir: "client/static", - }, - editor: { - entry: ".nanoforge/editor/client/main.ts", - save: ".nanoforge/client.save.json", - }, - dirs: { - components: "client/components", - systems: "client/systems", - }, - }, - server: { - enable: false, - outDir: ".nanoforge/server", - build: { - entry: "server/main.ts", - staticDir: "server/static", - }, - editor: { - entry: ".nanoforge/editor/server/main.ts", - save: ".nanoforge/server.save.json", - }, - dirs: { - components: "server/components", - systems: "server/systems", - }, - }, -}; diff --git a/src/lib/config/config-loader.spec.ts b/src/lib/config/config-loader.spec.ts index d61c13b..b39997a 100644 --- a/src/lib/config/config-loader.spec.ts +++ b/src/lib/config/config-loader.spec.ts @@ -1,105 +1,49 @@ -import { plainToInstance } from "class-transformer"; -import { validate } from "class-validator"; -import { existsSync, readFileSync } from "node:fs"; +import { parseConfig } from "@nanoforge-dev/config"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { loadConfig } from "./config-loader"; + vi.mock("node:fs", () => ({ existsSync: vi.fn(), - readFileSync: vi.fn(), -})); - -vi.mock("class-transformer", () => ({ - plainToInstance: vi.fn(), - Expose: () => () => {}, - Type: () => () => {}, })); -vi.mock("class-validator", () => ({ - validate: vi.fn(), - IsBoolean: () => () => {}, - IsEnum: () => () => {}, - IsNotEmpty: () => () => {}, - IsPort: () => () => {}, - IsString: () => () => {}, - ValidateNested: () => () => {}, +vi.mock("@nanoforge-dev/config", () => ({ + parseConfig: vi.fn(), })); describe("loadConfig", () => { beforeEach(() => { vi.resetAllMocks(); - vi.resetModules(); }); - it("should load config from a named file", async () => { - const rawConfig = { name: "test-app", language: "ts" }; - vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig)); - vi.mocked(plainToInstance).mockReturnValue(rawConfig as any); - vi.mocked(validate).mockResolvedValue([]); + it("loads nanoforge.config.ts when it exists", async () => { + vi.mocked(existsSync).mockImplementation((path) => String(path).endsWith(".ts")); + const resolved = { type: "client" as const }; + vi.mocked(parseConfig).mockResolvedValue(resolved); - const { loadConfig: freshLoad } = await import("./config-loader"); - const result = await freshLoad("/project", "custom.json"); + const result = await loadConfig("/project"); - expect(readFileSync).toHaveBeenCalledWith("/project/custom.json", "utf-8"); - expect(result).toEqual(rawConfig); + expect(parseConfig).toHaveBeenCalledWith(join("/project", "nanoforge.config.ts")); + expect(result).toBe(resolved); }); - it("should search for nanoforge.config.json when no name provided", async () => { - const rawConfig = { name: "my-app" }; - vi.mocked(existsSync).mockReturnValue(true); - vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig)); - vi.mocked(plainToInstance).mockReturnValue(rawConfig as any); - vi.mocked(validate).mockResolvedValue([]); + it("falls back to nanoforge.config.js when no .ts file exists", async () => { + vi.mocked(existsSync).mockImplementation((path) => String(path).endsWith(".js")); + const resolved = { type: "server" as const }; + vi.mocked(parseConfig).mockResolvedValue(resolved); - const { loadConfig: freshLoad } = await import("./config-loader"); - const result = await freshLoad("/project"); + const result = await loadConfig("/project"); - expect(existsSync).toHaveBeenCalled(); - expect(result).toEqual(rawConfig); + expect(parseConfig).toHaveBeenCalledWith(join("/project", "nanoforge.config.js")); + expect(result).toBe(resolved); }); - it("should throw when no config file is found", async () => { + it("throws when neither nanoforge.config.ts nor .js exists", async () => { vi.mocked(existsSync).mockReturnValue(false); - const { loadConfig: freshLoad } = await import("./config-loader"); - - await expect(freshLoad("/project")).rejects.toThrow("Configuration file not found at path:"); - }); - - it("should throw when config file cannot be parsed", async () => { - vi.mocked(readFileSync).mockReturnValue("invalid json"); - vi.mocked(existsSync).mockReturnValue(true); - - const { loadConfig: freshLoad } = await import("./config-loader"); - - await expect(freshLoad("/project")).rejects.toThrow("File System Error [read config file]"); - }); - - it("should throw on validation errors", async () => { - const rawConfig = { name: "" }; - vi.mocked(existsSync).mockReturnValue(true); - vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig)); - vi.mocked(plainToInstance).mockReturnValue(rawConfig as any); - vi.mocked(validate).mockResolvedValue([ - { property: "name", toString: () => "name must not be empty" } as any, - ]); - - const { loadConfig: freshLoad } = await import("./config-loader"); - - await expect(freshLoad("/project")).rejects.toThrow("Invalid config"); - }); - - it("should cache config after first load", async () => { - const rawConfig = { name: "cached-app" }; - vi.mocked(existsSync).mockReturnValue(true); - vi.mocked(readFileSync).mockReturnValue(JSON.stringify(rawConfig)); - vi.mocked(plainToInstance).mockReturnValue(rawConfig as any); - vi.mocked(validate).mockResolvedValue([]); - - const { loadConfig: freshLoad } = await import("./config-loader"); - - await freshLoad("/project"); - await freshLoad("/project"); - - expect(readFileSync).toHaveBeenCalledTimes(1); + await expect(loadConfig("/project")).rejects.toThrow("Configuration file not found at path:"); + expect(parseConfig).not.toHaveBeenCalled(); }); }); diff --git a/src/lib/config/config-loader.ts b/src/lib/config/config-loader.ts index 86ae6d3..e7697f0 100644 --- a/src/lib/config/config-loader.ts +++ b/src/lib/config/config-loader.ts @@ -1,54 +1,26 @@ -import { plainToInstance } from "class-transformer"; -import { validate } from "class-validator"; -import { existsSync, readFileSync } from "node:fs"; +import { type NanoforgeConfig, parseConfig } from "@nanoforge-dev/config"; +import { existsSync } from "node:fs"; import { join } from "node:path"; -import { CONFIG_FILE_NAME } from "@lib/constants"; +import { ConfigNotFoundError } from "@utils/errors"; -import { CLIError, ConfigNotFoundError, FileSystemError } from "@utils/errors"; -import { deepMerge } from "@utils/object"; +const CONFIG_BASE_NAME = "nanoforge.config"; +const CONFIG_EXTENSIONS = ["ts", "js"]; -import { CONFIG_DEFAULTS } from "./config-defaults"; -import { Config } from "./config.type"; - -let config: Config | null; - -const getConfigPath = (directory: string, name?: string) => { - if (name) { - return join(directory, name); - } else { - for (const n of [CONFIG_FILE_NAME]) { - const path = join(directory, n); - if (existsSync(path)) return path; - } - throw new ConfigNotFoundError(join(directory, CONFIG_FILE_NAME)); +const getConfigPath = (directory: string): string => { + for (const ext of CONFIG_EXTENSIONS) { + const path = join(directory, `${CONFIG_BASE_NAME}.${ext}`); + if (existsSync(path)) return path; } + throw new ConfigNotFoundError(join(directory, `${CONFIG_BASE_NAME}.ts`)); }; -export const loadConfig = async ( - directory: string, - name?: string, - noThrow: boolean = false, -): Promise => { - if (config) return config; - - let rawData; - - const path = getConfigPath(directory, name); - try { - rawData = deepMerge(CONFIG_DEFAULTS, JSON.parse(readFileSync(path, "utf-8"))); - } catch { - rawData = noThrow ? CONFIG_DEFAULTS : null; - } - if (!rawData) throw new FileSystemError("read config file", path); - - const data = plainToInstance(Config, rawData, { - excludeExtraneousValues: true, - }); - const errors = await validate(data); - if (errors.length > 0) - throw new CLIError(`Invalid config:\n${errors.toString().replace(/,/g, "\n")}`); - - config = data; - return config; +/** + * Looks up a `nanoforge.config.ts` file in `directory`, falling back to + * `nanoforge.config.js` when no `.ts` file exists. Throws a + * `ConfigNotFoundError` when neither is present. + */ +export const loadConfig = async (directory: string): Promise => { + const path = getConfigPath(directory); + return parseConfig(path); }; diff --git a/src/lib/config/config.type.ts b/src/lib/config/config.type.ts deleted file mode 100644 index b50435c..0000000 --- a/src/lib/config/config.type.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Expose, Type } from "class-transformer"; -import { IsBoolean, IsEnum, IsNotEmpty, IsPort, IsString, ValidateNested } from "class-validator"; - -export class BuildConfig { - @Expose() - @IsString() - @IsNotEmpty() - entry!: string; - - @Expose() - @IsString() - @IsNotEmpty() - staticDir!: string; -} - -export class EditorConfig { - @Expose() - @IsString() - @IsNotEmpty() - entry!: string; - - @Expose() - @IsString() - @IsNotEmpty() - save!: string; -} - -export class DirsConfig { - @Expose() - @IsString() - @IsNotEmpty() - components!: string; - - @Expose() - @IsString() - @IsNotEmpty() - systems!: string; -} - -export class ClientConfig { - @Expose() - @IsBoolean() - enable!: boolean; - - @Expose() - @IsPort() - port!: string; - - @Expose() - @IsString() - @IsNotEmpty() - outDir!: string; - - @Expose() - @Type(() => BuildConfig) - @ValidateNested() - build!: BuildConfig; - - @Expose() - @Type(() => EditorConfig) - @ValidateNested() - editor!: EditorConfig; - - @Expose() - @Type(() => DirsConfig) - @ValidateNested() - dirs!: DirsConfig; -} - -export class ServerConfig { - @Expose() - @IsBoolean() - enable!: boolean; - - @Expose() - @IsString() - @IsNotEmpty() - outDir!: string; - - @Expose() - @Type(() => BuildConfig) - @ValidateNested() - build!: BuildConfig; - - @Expose() - @Type(() => EditorConfig) - @ValidateNested() - editor!: EditorConfig; - - @Expose() - @Type(() => DirsConfig) - @ValidateNested() - dirs!: DirsConfig; -} - -export class SslConfig { - @Expose() - @IsBoolean() - enable!: boolean; - - @Expose() - @IsString() - cert!: string; - - @Expose() - @IsString() - key!: string; -} - -export class Config { - @Expose() - @IsString() - @IsNotEmpty() - name!: string; - - @Expose() - @IsEnum(["ts", "js"]) - language!: "ts" | "js"; - - @Expose() - @IsBoolean() - initFunctions!: boolean; - - @Expose() - @Type(() => ClientConfig) - @ValidateNested() - client!: ClientConfig; - - @Expose() - @Type(() => ServerConfig) - @ValidateNested() - server!: ServerConfig; - - @Expose() - @Type(() => SslConfig) - @ValidateNested() - ssl!: SslConfig; -} diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index aacfbbc..a8646a7 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -1,2 +1,6 @@ -export * from "./config.type"; export * from "./config-loader"; +export * from "./workspace-config-parser"; +export * from "./workspace-config.types"; +export * from "./workspace-loader"; +export { ConfigParseError } from "@nanoforge-dev/config"; +export type { NanoforgeConfig as Config } from "@nanoforge-dev/config"; diff --git a/src/lib/config/workspace-config-parser.spec.ts b/src/lib/config/workspace-config-parser.spec.ts new file mode 100644 index 0000000..5617a67 --- /dev/null +++ b/src/lib/config/workspace-config-parser.spec.ts @@ -0,0 +1,133 @@ +import { existsSync, statSync } from "node:fs"; +import { glob } from "node:fs/promises"; +import { join } from "node:path"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { ConfigNotFoundError } from "@utils/errors"; + +import { loadConfig } from "./config-loader"; +import { parseWorkspaceConfig } from "./workspace-config-parser"; + +vi.mock("node:fs", () => ({ + existsSync: vi.fn(), + statSync: vi.fn(), +})); + +vi.mock("node:fs/promises", () => ({ + glob: vi.fn(), +})); + +vi.mock("./config-loader", () => ({ + loadConfig: vi.fn(), +})); + +const asDirectory = () => ({ isDirectory: () => true }); + +const asyncIterableOf = (values: T[]): AsyncIterable => ({ + [Symbol.asyncIterator]: async function* () { + for (const value of values) yield value; + }, +}); + +describe("parseWorkspaceConfig", () => { + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(existsSync).mockReturnValue(true); + vi.mocked(statSync).mockReturnValue(asDirectory() as ReturnType); + }); + + it("resolves a root project config", async () => { + const config = { type: "client" as const }; + vi.mocked(loadConfig).mockResolvedValue(config); + + const result = await parseWorkspaceConfig("/app"); + + expect(result).toEqual({ type: "project", config }); + }); + + it("throws when the root config is a lib", async () => { + vi.mocked(loadConfig).mockResolvedValue({ type: "lib" as const }); + + await expect(parseWorkspaceConfig("/app")).rejects.toThrow( + "the root config cannot be of type 'lib'", + ); + }); + + it("resolves every directory a package pattern matches", async () => { + const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] }; + const clientConfig = { type: "client" as const }; + const libConfig = { type: "lib" as const }; + + vi.mocked(glob).mockReturnValue( + asyncIterableOf(["apps/client", "apps/foo-lib"]) as ReturnType, + ); + vi.mocked(loadConfig).mockImplementation(async (directory: string) => { + if (directory === "/app") return workspaceConfig; + if (directory === join("/app", "apps/client")) return clientConfig; + if (directory === join("/app", "apps/foo-lib")) return libConfig; + throw new Error(`unexpected directory: ${directory}`); + }); + + const result = await parseWorkspaceConfig("/app"); + + expect(result).toEqual({ + type: "workspace", + config: workspaceConfig, + projects: { + "apps/client": { type: "project", config: clientConfig }, + "apps/foo-lib": { type: "lib", config: libConfig }, + }, + }); + }); + + it("skips a matched directory with no config instead of throwing", async () => { + const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] }; + const clientConfig = { type: "client" as const }; + + vi.mocked(glob).mockReturnValue( + asyncIterableOf(["apps/client", "apps/empty"]) as ReturnType, + ); + vi.mocked(loadConfig).mockImplementation(async (directory: string) => { + if (directory === "/app") return workspaceConfig; + if (directory === join("/app", "apps/client")) return clientConfig; + throw new ConfigNotFoundError(join(directory, "nanoforge.config.ts")); + }); + + const result = await parseWorkspaceConfig("/app"); + + expect(result).toEqual({ + type: "workspace", + config: workspaceConfig, + projects: { + "apps/client": { type: "project", config: clientConfig }, + }, + }); + }); + + it("warns instead of throwing when a pattern matches no project or lib", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] }; + + vi.mocked(glob).mockReturnValue(asyncIterableOf([]) as ReturnType); + vi.mocked(loadConfig).mockResolvedValue(workspaceConfig); + + const result = await parseWorkspaceConfig("/app"); + + expect(result).toEqual({ type: "workspace", config: workspaceConfig, projects: {} }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("apps/*")); + }); + + it("throws when a matched package is itself a workspace", async () => { + const workspaceConfig = { type: "workspace" as const, packages: ["apps/*"] }; + + vi.mocked(glob).mockReturnValue(asyncIterableOf(["apps/nested"]) as ReturnType); + vi.mocked(loadConfig).mockImplementation(async (directory: string) => { + if (directory === "/app") return workspaceConfig; + return { type: "workspace" as const }; + }); + + await expect(parseWorkspaceConfig("/app")).rejects.toThrow( + "a workspace package cannot itself be of type 'workspace'", + ); + }); +}); diff --git a/src/lib/config/workspace-config-parser.ts b/src/lib/config/workspace-config-parser.ts new file mode 100644 index 0000000..eaa62d5 --- /dev/null +++ b/src/lib/config/workspace-config-parser.ts @@ -0,0 +1,87 @@ +import type { NanoforgeConfig } from "@nanoforge-dev/config"; +import { existsSync, statSync } from "node:fs"; +import { glob } from "node:fs/promises"; +import { join } from "node:path"; + +import { CLIError, ConfigNotFoundError } from "@utils/errors"; + +import { loadConfig } from "./config-loader"; +import type { + BaseWorkspaceConfig, + LibConfig, + ProjectConfig, + WorkspaceConfig, +} from "./workspace-config.types"; + +const resolvePackageConfig = async ( + packageDir: string, +): Promise => { + let config: NanoforgeConfig; + try { + config = await loadConfig(packageDir); + } catch (error) { + if (error instanceof ConfigNotFoundError) return undefined; + throw error; + } + + if (config.type === "workspace") { + throw new CLIError( + `Invalid config at '${packageDir}': a workspace package cannot itself be of type 'workspace'.`, + "Nest projects and libs under the root workspace instead of another workspace.", + ); + } + + return config.type === "lib" ? { type: "lib", config } : { type: "project", config }; +}; + +const resolvePackagePattern = async ( + directory: string, + pattern: string, +): Promise> => { + const projects: Record = {}; + + for await (const relativePath of glob(pattern, { cwd: directory })) { + const packageDir = join(directory, relativePath); + if (!existsSync(packageDir) || !statSync(packageDir).isDirectory()) continue; + + const entry = await resolvePackageConfig(packageDir); + if (entry) projects[relativePath] = entry; + } + + if (Object.keys(projects).length === 0) { + console.warn( + `[nanoforge] No project or lib config found for workspace package pattern '${pattern}' in '${directory}'.`, + ); + } + + return projects; +}; + +const parseBaseWorkspaceConfig = async ( + directory: string, + config: Extract, +): Promise => { + const patterns = config.packages ?? []; + + const projects: Record = {}; + for (const pattern of patterns) { + Object.assign(projects, await resolvePackagePattern(directory, pattern)); + } + + return { type: "workspace", config, projects }; +}; + +export const parseWorkspaceConfig = async (directory: string): Promise => { + const config = await loadConfig(directory); + + if (config.type === "lib") { + throw new CLIError( + `Invalid config at '${directory}': the root config cannot be of type 'lib'.`, + "Use a 'workspace' or a 'client'/'server' project config at the root.", + ); + } + + if (config.type === "workspace") return parseBaseWorkspaceConfig(directory, config); + + return { type: "project", config }; +}; diff --git a/src/lib/config/workspace-config.types.ts b/src/lib/config/workspace-config.types.ts new file mode 100644 index 0000000..1fe329e --- /dev/null +++ b/src/lib/config/workspace-config.types.ts @@ -0,0 +1,24 @@ +import type { + ClientConfig, + LibConfig as NfLibConfig, + WorkspaceConfig as NfWorkspaceConfig, + ServerConfig, +} from "@nanoforge-dev/config"; + +export interface ProjectConfig { + type: "project"; + config: ClientConfig | ServerConfig; +} + +export interface LibConfig { + type: "lib"; + config: NfLibConfig; +} + +export interface BaseWorkspaceConfig { + type: "workspace"; + config: NfWorkspaceConfig; + projects: Record; +} + +export type WorkspaceConfig = ProjectConfig | BaseWorkspaceConfig; diff --git a/src/lib/config/workspace-loader.ts b/src/lib/config/workspace-loader.ts new file mode 100644 index 0000000..df7d376 --- /dev/null +++ b/src/lib/config/workspace-loader.ts @@ -0,0 +1,28 @@ +import type { ClientConfig, ServerConfig } from "@nanoforge-dev/config"; +import { join } from "node:path"; + +import type { WorkspaceConfig } from "./workspace-config.types"; + +export interface ResolvedProject { + directory: string; + config: ClientConfig | ServerConfig; +} + +export const resolveProjects = ( + workspace: WorkspaceConfig, + baseDirectory: string, +): ResolvedProject[] => { + if (workspace.type === "project") { + return [{ directory: baseDirectory, config: workspace.config }]; + } + + return Object.entries(workspace.projects) + .filter( + (entry): entry is [string, Extract<(typeof entry)[1], { type: "project" }>] => + entry[1].type === "project", + ) + .map(([relativePath, project]) => ({ + directory: join(baseDirectory, relativePath), + config: project.config, + })); +}; diff --git a/src/lib/input/inputs/dev/generate.input.spec.ts b/src/lib/input/inputs/dev/generate.input.spec.ts deleted file mode 100644 index c49fc62..0000000 --- a/src/lib/input/inputs/dev/generate.input.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { type Input } from "../../input.type"; -import { getDevGenerateInput } from "./generate.input"; - -const createInput = (entries: [string, any][]): Input => { - return new Map(entries.map(([key, value]) => [key, { value }])); -}; - -describe("getDevGenerateInput", () => { - it("should return the generate value when provided", () => { - const input = createInput([["generate", true]]); - expect(getDevGenerateInput(input)).toBe(true); - }); - - it("should return false as default when generate is missing", () => { - const input = createInput([]); - expect(getDevGenerateInput(input)).toBe(false); - }); -}); diff --git a/src/lib/input/inputs/dev/generate.input.ts b/src/lib/input/inputs/dev/generate.input.ts deleted file mode 100644 index be1647f..0000000 --- a/src/lib/input/inputs/dev/generate.input.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getBooleanInputWithDefault } from "../../base-inputs"; -import { type Input } from "../../input.type"; - -export const getDevGenerateInput = (inputs: Input): boolean => { - return getBooleanInputWithDefault(inputs, "generate", false); -}; diff --git a/src/lib/input/inputs/dev/index.ts b/src/lib/input/inputs/dev/index.ts deleted file mode 100644 index c0d1098..0000000 --- a/src/lib/input/inputs/dev/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./generate.input"; diff --git a/src/lib/input/inputs/index.ts b/src/lib/input/inputs/index.ts index 18b6fde..607306f 100644 --- a/src/lib/input/inputs/index.ts +++ b/src/lib/input/inputs/index.ts @@ -7,7 +7,6 @@ export * from "./server.input"; export * from "./watch.input"; export * from "./create"; -export * from "./dev"; export * from "./editor"; export * from "./install"; export * from "./login-out"; diff --git a/src/lib/input/inputs/new/index.ts b/src/lib/input/inputs/new/index.ts index 9532ed4..af35a3a 100644 --- a/src/lib/input/inputs/new/index.ts +++ b/src/lib/input/inputs/new/index.ts @@ -1,5 +1,4 @@ export * from "./docker.input"; -export * from "./init-functions.input"; export * from "./language.input"; export * from "./lint.input"; export * from "./package-manager.input"; diff --git a/src/lib/input/inputs/new/init-functions.input.spec.ts b/src/lib/input/inputs/new/init-functions.input.spec.ts deleted file mode 100644 index 15c9d53..0000000 --- a/src/lib/input/inputs/new/init-functions.input.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { type Input } from "../../input.type"; -import { getNewInitFunctionsWithDefault } from "./init-functions.input"; - -const createInput = (entries: [string, any][]): Input => { - return new Map(entries.map(([key, value]) => [key, { value }])); -}; - -describe("getNewInitFunctionsWithDefault", () => { - it("should return the value when provided", () => { - const input = createInput([["initFunctions", true]]); - expect(getNewInitFunctionsWithDefault(input)).toBe(true); - }); - - it("should return false as default when missing", () => { - const input = createInput([]); - expect(getNewInitFunctionsWithDefault(input)).toBe(false); - }); -}); diff --git a/src/lib/input/inputs/new/init-functions.input.ts b/src/lib/input/inputs/new/init-functions.input.ts deleted file mode 100644 index 5c20e74..0000000 --- a/src/lib/input/inputs/new/init-functions.input.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getBooleanInputWithDefault } from "../../base-inputs"; -import { type Input } from "../../input.type"; - -export const getNewInitFunctionsWithDefault = (inputs: Input) => { - return getBooleanInputWithDefault(inputs, "initFunctions", false); -}; diff --git a/src/lib/schematics/nanoforge.collection.spec.ts b/src/lib/schematics/nanoforge.collection.spec.ts index a3dadeb..545e30e 100644 --- a/src/lib/schematics/nanoforge.collection.spec.ts +++ b/src/lib/schematics/nanoforge.collection.spec.ts @@ -11,29 +11,29 @@ describe("NanoforgeCollection", () => { it("should return all available schematics", () => { const schematics = collection.getSchematics(); const names = schematics.map((s) => s.name); - expect(names).toContain("application"); - expect(names).toContain("configuration"); - expect(names).toContain("part-base"); - expect(names).toContain("part-main"); + expect(names).toContain("project"); + expect(names).toContain("workspace"); + expect(names).toContain("component"); + expect(names).toContain("system"); }); it("should have aliases for each schematic", () => { const schematics = collection.getSchematics(); const aliases = schematics.map((s) => s.alias); - expect(aliases).toContain("application"); - expect(aliases).toContain("config"); - expect(aliases).toContain("base"); - expect(aliases).toContain("main"); + expect(aliases).toContain("project"); + expect(aliases).toContain("workspace"); + expect(aliases).toContain("component"); + expect(aliases).toContain("system"); }); }); describe("execute", () => { it("should accept a valid schematic name", async () => { - await expect(collection.execute("application", [])).resolves.not.toThrow(); + await expect(collection.execute("project", [])).resolves.not.toThrow(); }); it("should accept a valid schematic alias", async () => { - await expect(collection.execute("config", [])).resolves.not.toThrow(); + await expect(collection.execute("workspace", [])).resolves.not.toThrow(); }); it("should reject an invalid schematic name", async () => { diff --git a/src/lib/schematics/nanoforge.collection.ts b/src/lib/schematics/nanoforge.collection.ts index 4989cec..16731ff 100644 --- a/src/lib/schematics/nanoforge.collection.ts +++ b/src/lib/schematics/nanoforge.collection.ts @@ -14,29 +14,14 @@ export interface Schematic { export class NanoforgeCollection extends AbstractCollection { private static schematics: Schematic[] = [ { - name: "application", - alias: "application", - description: "Generate a new application", + name: "project", + alias: "project", + description: "Generate a new project", }, { - name: "configuration", - alias: "config", - description: "Generate a CLI configuration file", - }, - { - name: "part-base", - alias: "base", - description: "Generate a NanoForge Part Base", - }, - { - name: "part-main", - alias: "main", - description: "Generate a NanoForge Part Main file", - }, - { - name: "docker", - alias: "docker", - description: "Generate a Dockerfile for the application", + name: "workspace", + alias: "workspace", + description: "Generate a new workspace", }, { name: "component", diff --git a/src/lib/ui/messages.ts b/src/lib/ui/messages.ts index 8b5d426..a138117 100644 --- a/src/lib/ui/messages.ts +++ b/src/lib/ui/messages.ts @@ -57,12 +57,6 @@ export const Messages = { CREATE_FAILED: failure("Creation failed!"), CREATE_NAME_QUESTION: "What is the name of your component/system?", - // --- Generate --- - GENERATE_START: "NanoForge Generate", - GENERATE_SUCCESS: success("Generation succeeded!"), - GENERATE_FAILED: failure("Generation failed!"), - GENERATE_WATCH_START: "Watching for changes...", - // --- Dev --- DEV_START: "NanoForge Dev Mode", DEV_SUCCESS: "Dev mode ended", diff --git a/tsdown.config.ts b/tsdown.config.ts index 4d95f0a..073c2e3 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -21,9 +21,9 @@ function createTsdownConfig({ platform: "node", target: "esnext", treeshake: false, - deps: { - skipNodeModulesBundle: true, - }, + // No `deps.neverBundle: true` here: it externalizes anything matching tsdown's + // npm-package-specifier heuristic, which our `@lib/*` and `@utils/*` path aliases + // match too, leaving them as unresolvable imports in the built output. env: dotenv.config({ path: resolve( process.cwd(), diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..42e96c0 --- /dev/null +++ b/turbo.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://turbo.build/schema.json", + "remoteCache": { + "enabled": true + }, + "globalDependencies": ["tsconfig.json"], + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "tsdown.config.ts"], + "outputs": ["dist/**"], + "outputLogs": "errors-only" + }, + "build:dev": { + "dependsOn": ["^build:dev"], + "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "tsdown.config.ts"], + "outputs": ["dist/**"], + "outputLogs": "errors-only" + }, + "format": { + "dependsOn": ["^format"], + "inputs": ["**"], + "outputs": [], + "outputLogs": "errors-only" + }, + "lint": { + "dependsOn": ["^lint"], + "inputs": ["**"], + "outputs": [], + "outputLogs": "errors-only" + }, + "test:unit": { + "dependsOn": ["^test:unit"], + "inputs": [ + "src/**", + "test/**", + "package.json", + "tsconfig.json", + "tsdown.config.ts", + "vitest.config.ts" + ], + "outputs": [], + "outputLogs": "errors-only" + }, + "test:e2e": { + "dependsOn": ["build", "^test:e2e"], + "inputs": [ + "src/**", + "e2e/**", + "package.json", + "tsconfig.json", + "tsdown.config.ts", + "vitest.config.e2e.ts" + ], + "outputs": [], + "outputLogs": "errors-only" + }, + "docs": { + "dependsOn": ["^docs"], + "inputs": ["src/**", "test/**", "package.json", "tsconfig.json", "../../docs/templates/**"], + "outputs": ["../../docs/references/**"], + "outputLogs": "errors-only" + } + } +}