diff --git a/.changeset/auto-changeset-swarm-wasp.md b/.changeset/auto-changeset-swarm-wasp.md new file mode 100644 index 00000000..7367df1a --- /dev/null +++ b/.changeset/auto-changeset-swarm-wasp.md @@ -0,0 +1,10 @@ +--- +"@ingenyus/swarm-wasp": major +--- + +- feat!: initialise Wasp 0.24 compatibility ([b3e8bb9](https://github.com/Genyus/swarm/commit/b3e8bb9b3bd9cd34f2ff3629246953524632735f)) +- feat!: implement core Wasp 0.24 updates ([2f3b5f2](https://github.com/Genyus/swarm/commit/2f3b5f2132805488629b9a1eb225e224920bbbad)) +- feat!: update templates and generators ([53daa42](https://github.com/Genyus/swarm/commit/53daa420ac894b6911e14144ff8b9dd8fa83f3d5)) +- feat!: update project scaffolding ([dfbf304](https://github.com/Genyus/swarm/commit/dfbf304f803d227eec2e13795c62bfc8992e9427)) + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1a1f47a..f90bfa59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,21 +23,21 @@ jobs: strategy: matrix: - node-version: [20, 22, 24] + node-version: [22, 24, 26] steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: ${{ matrix.node-version }} - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Get pnpm store directory shell: bash @@ -45,7 +45,7 @@ jobs: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache - uses: actions/cache@v4 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -84,18 +84,18 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 token: ${{ secrets.PAT_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index 3171de5a..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "arrowParens": "always", - "endOfLine": "lf" -} diff --git a/AGENTS.md b/AGENTS.md index 3e1ded2f..c8f3f42d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,11 +21,11 @@ Swarm is a set of code generation tools for rapid Typescript application develop ### Key Scripts - `pnpm build` - Build all packages - `pnpm dev` - Watch mode for development -- `pnpm lint` - Run ESLint -- `pnpm lint:fix` - Fix linting issues automatically +- `pnpm lint` - Run Biome lint + format checks +- `pnpm lint:fix` - Fix linting/formatting issues automatically - `pnpm typecheck` - Run TypeScript type checking -- `pnpm format` - Apply Prettier formatting -- `pnpm format:check` - Check Prettier formatting +- `pnpm format` - Apply Biome formatting +- `pnpm format:check` - Check Biome formatting ## Testing @@ -81,7 +81,7 @@ Swarm is a set of code generation tools for rapid Typescript application develop ### Key Dependencies - **Vite** - Build tool and dev server -- **ESLint** - Linting +- **Biome** - Linting and formatting - **TypeScript** - Type checking ### Development Tools diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ef5c66e..ee17f851 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Thank you for your interest in contributing to Swarm! This document outlines bes --- ## Code Style -- Use [Prettier](https://prettier.io/) and [ESLint](https://eslint.org/) for formatting and linting. +- Use [Biome](https://biomejs.dev/) for formatting and linting. - Prefer single responsibility per file/module. - Use descriptive variable and function names. - Write JSDoc comments for all exported functions and types. diff --git a/README.md b/README.md index bf20eebb..e73587c1 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ To integrate Swarm components into your own projects or to extend the framework, ## Scripts & Tooling - **Monorepo management:** [pnpm](https://pnpm.io/) (`pnpm-workspace.yaml`) -- **Code style and linting:** [ESLint](https://eslint.org), [Prettier](https://prettier.io), [EditorConfig](https://editorconfig.org) +- **Code style and linting:** [Biome](https://biomejs.dev), [EditorConfig](https://editorconfig.org) - **Schema management:** [Zod](https://zod.dev) - **Testing:** [Vitest](https://vitest.dev) - **CI/CD:** [GitHub Actions](https://docs.github.com/en/actions) diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..3327b997 --- /dev/null +++ b/biome.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.3/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "packages/**", + "!**/dist", + "!**/coverage", + "!**/src/templates", + "!**/tests/output", + "!**/tests/mcp/output", + "!**/*.d.ts" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80, + "lineEnding": "lf" + }, + "linter": { + "enabled": true, + "rules": { + "preset": "recommended", + "correctness": { + "noUnusedVariables": "error" + }, + "style": { + "useConst": "error", + "noNonNullAssertion": "warn" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "es5", + "arrowParentheses": "always" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "overrides": [ + { + "includes": ["**/*.test.ts", "**/tests/**"], + "linter": { + "rules": { + "complexity": { + "useLiteralKeys": "off" + }, + "style": { + "noNonNullAssertion": "off" + }, + "suspicious": { + "noExplicitAny": "off" + } + } + } + } + ] +} diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index 30931dc1..00000000 --- a/eslint.config.js +++ /dev/null @@ -1,70 +0,0 @@ -import js from '@eslint/js'; -import typescript from '@typescript-eslint/eslint-plugin'; -import typescriptParser from '@typescript-eslint/parser'; -import prettierConfig from 'eslint-config-prettier'; - -export default [ - js.configs.recommended, - { - files: ['packages/*/src/**/*.ts', 'packages/*/tests/**/*.ts'], - languageOptions: { - parser: typescriptParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - // Don't specify project here - let individual packages handle it - }, - globals: { - __dirname: 'readonly', - process: 'readonly', - console: 'readonly', - Buffer: 'readonly', - require: 'readonly', - module: 'readonly', - BufferEncoding: 'readonly', - vi: 'readonly', - describe: 'readonly', - it: 'readonly', - test: 'readonly', - expect: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly', - NodeJS: 'readonly', - global: 'readonly', - }, - }, - plugins: { - '@typescript-eslint': typescript, - }, - rules: { - ...typescript.configs.recommended.rules, - ...prettierConfig.rules, - // Disable base no-unused-vars in favor of @typescript-eslint/no-unused-vars - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_' }, - ], - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-non-null-assertion': 'warn', - 'prefer-const': 'error', - 'no-var': 'error', - 'no-console': 'off', - }, - }, - { - ignores: [ - 'dist/', - 'node_modules/', - 'coverage/', - 'tests/output/', - 'packages/*/dist/', - 'packages/*/node_modules/', - 'packages/*/coverage/', - 'packages/*/tests/out/', - ], - }, -]; diff --git a/knip.json b/knip.json index 880fae2e..fe0741e4 100644 --- a/knip.json +++ b/knip.json @@ -1,9 +1,10 @@ { "ignore": [ ".changeset/**/*", - ".github/changeset-version.js", "apps/swarm-wasp-starter/**/*", - "packages/swarm/src/mcp/cli/**/index.ts" + "packages/swarm/src/mcp/cli/**/index.ts", + "packages/swarm-wasp/src/wasp-config/stubs/**/index.ts" ], - "ignoreDependencies": ["@commander-js/extra-typings", "wasp-config"] + "ignoreBinaries": ["wasp"], + "ignoreDependencies": ["@commander-js/extra-typings", "@wasp.sh/spec"] } diff --git a/package.json b/package.json index 656b1514..f07638f6 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "scripts": { "build": "pnpm -r run build", "test": "pnpm -r run test", - "lint": "pnpm -r run lint", - "lint:fix": "pnpm -r run lint:fix", - "format": "pnpm -r run format", - "format:check": "pnpm -r run format:check", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "format:check": "biome format .", "typecheck": "pnpm -r run typecheck", "changeset": "changeset", "changeset:auto": "node .changeset/autogenerate.js", @@ -38,16 +38,11 @@ "pnpm": ">=10.0.0" }, "devDependencies": { - "@changesets/cli": "^2.29.7", - "@eslint/js": "^9.39.1", - "@types/node": "^24.10.1", - "@typescript-eslint/eslint-plugin": "^8.46.4", - "@typescript-eslint/parser": "^8.46.4", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "knip": "^5.69.1", - "prettier": "^3.6.2", - "typescript": "^5.9.3" + "@biomejs/biome": "2.5.3", + "@changesets/cli": "^2.31.0", + "@types/node": "^26.1.1", + "knip": "^6.26.0", + "typescript": "^7.0.2" }, "packageManager": "pnpm@10.20.0" } diff --git a/packages/swarm-wasp/.prettierrc.json b/packages/swarm-wasp/.prettierrc.json deleted file mode 100644 index 3171de5a..00000000 --- a/packages/swarm-wasp/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "arrowParens": "always", - "endOfLine": "lf" -} diff --git a/packages/swarm-wasp/README.md b/packages/swarm-wasp/README.md index 155a9512..fab77ace 100644 --- a/packages/swarm-wasp/README.md +++ b/packages/swarm-wasp/README.md @@ -66,14 +66,21 @@ To disable the plugin or any provided generators, set the `disabled` property (` ### Wasp Version Compatibility -This package requires Wasp version **0.18.x** or **0.19.x** to be installed. The generators will automatically check your installed Wasp version and throw an error if it's incompatible. +This package requires Wasp version **0.24.x** to be installed. The generators will automatically check your installed Wasp version and throw an error if it's incompatible. To check your Wasp version: ```bash wasp version ``` -If you need to install or update Wasp, follow the instructions at [wasp.sh](https://wasp.sh). +To install or update Wasp: +```bash +npm install -g @wasp.sh/wasp-cli@latest +``` + +The quickest way to start a new project is the [Swarm Wasp Starter](https://github.com/Genyus/swarm-wasp-starter), which ships a preconfigured Wasp 0.24 app ready for extension and customisation. + +> **Upgrading from Wasp < 0.24?** Wasp 0.24 replaced the class-based TS config with the functional [Wasp Spec](https://wasp.sh/docs/general/wasp-ts-config) (`@wasp.sh/spec`). Feature config files now export a native `spec` array instead of a `configureFeature(app)` function — see [Enhanced Configuration](#enhanced-configuration) below. Existing `feature.wasp.ts` files are not migrated automatically. ## Features @@ -103,98 +110,73 @@ The Wasp plugin provides a number of improvements to standard Wasp functionality ### Enhanced Configuration -**Wasp:** Requires a single, monolithic `main.wasp` (or `main.wasp.ts`) file that defines all Wasp components for the application +**Wasp:** Wasp 0.24 supports splitting the [Wasp Spec](https://wasp.sh/docs/general/wasp-ts-config) across multiple `*.wasp.ts` files, but requires each one to be manually imported into `main.wasp.ts`. -**Swarm:** Only supports a `main.wasp.ts` file for application-level configuration, with additional `feature.wasp.ts` files that configure Wasp components for a single feature directory, positioning declarations with the relevant application features. Swarm provides an extended `App` class with fluent helper methods for more concise, readable configuration files and even sorts helper method calls for easier scanning. +**Swarm:** Keeps `main.wasp.ts` for application-level configuration and places a `feature.wasp.ts` file in each feature directory, co-locating declarations with the feature they configure. Each feature file exports an array of definitions, which Swarm **auto-discovers** and merges into the main spec. -`main.wasp.ts`: +`main.wasp.ts` (at the project root): ```typescript -import { App } from "@ingenyus/swarm-wasp"; +import { app } from "@wasp.sh/spec"; +import { Layout } from "./src/shared/client/components/Layout" with { type: "ref" }; +import { featureSpecs } from "./src/features/index.wasp"; -const app = await App.create("my-app", { +export default app({ + name: "my-app", title: "My Application", - wasp: { version: "^0.18.1" }, + wasp: { version: "^0.24.0" }, + auth: { + userEntity: "User", + methods: { email: {} }, + onAuthFailedRedirectTo: "/login", + }, + client: { rootComponent: Layout }, + spec: [featureSpecs], }); - -app - .auth({ method: "email" }) - .client({ - rootComponent: { - importDefault: "Layout", - from: "@src/shared/client/components/Layout", - }, - }); - -export default app; ``` -`feature.wasp.ts`: +`src/features//feature.wasp.ts`: ```typescript -import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app - // Action definitions - .addAction(feature, "createTask", { - entities: ["Task"], - auth: true, - }) - .addAction(feature, "updateTask", { - entities: ["Task"], - auth: true, - }) - // API definitions - .addApi(feature, "getTasks", { - method: "GET", - route: "undefined", - entities: ["Task"], - auth: true, - customMiddleware: true, - }) - // CRUD definitions - .addCrud(feature, "Tasks", { - entity: "Task", - get: { - isPublic: true - }, - getAll: { - isPublic: true - }, - create: { - override: true - }, - update: { - override: true - }, - }) - // Query definitions - .addQuery(feature, "getUserTasks", { - entities: ["Task"], - auth: true, - }); -} -``` +import { type Spec, action, api, crud, query } from "@wasp.sh/spec"; +import { createTask, updateTask } from "./server/actions/..." with { type: "ref" }; +import { getTasks } from "./server/apis/getTasks" with { type: "ref" }; +import { getTasksMiddleware } from "./server/apis/middleware/getTasks" with { type: "ref" }; +import { getUserTasks } from "./server/queries/getUserTasks" with { type: "ref" }; + +export const spec: Spec = [ + // Action definitions + action(createTask, { entities: ["Task"], auth: true }), + action(updateTask, { entities: ["Task"], auth: true }), + // Api definitions + api("GET", "/tasks", getTasks, { middlewareConfigFn: getTasksMiddleware, entities: ["Task"], auth: true }), + // Crud definitions + crud("Tasks", "Task", { get: { isPublic: true }, getAll: { isPublic: true }, create: { overrideFn: createTask }, update: { overrideFn: updateTask } }), + // Query definitions + query(getUserTasks, { entities: ["Task"], auth: true }), +]; +``` + +`src/features/index.wasp.ts` (generated — do not edit): + +```typescript +import type { Spec } from "@wasp.sh/spec"; -#### Available Methods +import { spec as tasksSpec } from "./tasks/feature.wasp"; -- `.addRoute()` - Simplified route creation with automatic component imports -- `.addApi()` - API endpoint creation with middleware support -- `.addCrud()` - CRUD operations with custom overrides -- `.addAction()` - Action creation with entity access -- `.addQuery()` - Query creation with authentication -- `.addJob()` - Background job creation with cron scheduling -- `.addApiNamespace()` - API namespace creation with middleware +export const featureSpecs: Spec = [tasksSpec]; +``` -#### :warning: Typescript Configuration +#### Generated declaration types -By default, Wasp only supports a monolithic `main.wasp.ts` file, but Swarm enables this to be broken up into multiple files. This is preconfigured if you're using the [Swarm Wasp Starter](https://github.com/Genyus/swarm-wasp-starter), but if you're configuring Swarm in your own project, you must first make the following change to `tsconfig.wasp.json`: +Swarm's generators emit native `@wasp.sh/spec` constructors into the feature's `spec` array (adding the matching `with { type: "ref" }` imports), so you get concise, convention-driven output without hand-writing import paths: -```diff -- "include": ["main.wasp.ts"] -+ "include": ["main.wasp.ts", "src/**/feature.wasp.ts"] -``` +- `route()` - Route + page with automatic component imports +- `api()` - API endpoint with optional custom middleware +- `apiNamespace()` - API namespace with middleware +- `crud()` - CRUD operations with public/override options +- `action()` / `query()` - operations with entity access and auth +- `job()` - background job with cron scheduling ### Clean Directory Structure @@ -203,30 +185,30 @@ By default, Wasp only supports a monolithic `main.wasp.ts` file, but Swarm enabl **Swarm:** Imposes a feature-based structure, with self-contained feature directories holding client- and server-side components, plus Wasp configuration files ``` -src/ -├── features/ -│ └── / -│ ├── feature.wasp.ts # Feature-level Wasp configuration -│ ├── client/ -│ │ └── components/ # General components -│ │ └── pages/ # Page components -│ └── server/ -│ ├── actions/ # Actions -│ ├── apis/ # API Endpoints -│ ├── cruds/ # CRUD Operations -│ ├── jobs/ # Background Jobs -│ ├── middleware/ # API Middleware -│ └── queries/ # Queries -├── shared/ -│ ├── client/ -│ │ ├── components/ # Shared React components -│ │ ├── hooks/ # Custom React hooks -│ │ └── lib/ # Utility functions -│ └── server/ -│ └── middleware/ # Global middleware - -├── main.wasp.ts # Application-level Wasp configuration -└── schema.prisma # Database schema +├── main.wasp.ts # Application-level Wasp configuration (project root) +├── schema.prisma # Database schema +└── src/ + ├── features/ + │ ├── index.wasp.ts # Generated features barrel (do not edit) + │ └── / + │ ├── feature.wasp.ts # Feature-level Wasp configuration + │ ├── client/ + │ │ ├── components/ # General components + │ │ └── pages/ # Page components + │ └── server/ + │ ├── actions/ # Actions + │ ├── apis/ # API Endpoints + │ ├── cruds/ # CRUD Operations + │ ├── jobs/ # Background Jobs + │ ├── middleware/ # API Middleware + │ └── queries/ # Queries + └── shared/ + ├── client/ + │ ├── components/ # Shared React components + │ ├── hooks/ # Custom React hooks + │ └── lib/ # Utility functions + └── server/ + └── middleware/ # Global middleware ``` ### Consistent File Structure diff --git a/packages/swarm-wasp/eslint.config.js b/packages/swarm-wasp/eslint.config.js deleted file mode 100644 index a685b673..00000000 --- a/packages/swarm-wasp/eslint.config.js +++ /dev/null @@ -1,56 +0,0 @@ -import typescript from '@typescript-eslint/eslint-plugin'; -import typescriptParser from '@typescript-eslint/parser'; -import baseConfig from '../../eslint.config.js'; - -export default [ - ...baseConfig, - { - files: ['src/**/*.ts', 'tests/**/*.ts'], - languageOptions: { - parser: typescriptParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - project: './tsconfig.json', - }, - globals: { - __dirname: 'readonly', - process: 'readonly', - console: 'readonly', - Buffer: 'readonly', - require: 'readonly', - module: 'readonly', - BufferEncoding: 'readonly', - vi: 'readonly', - describe: 'readonly', - it: 'readonly', - test: 'readonly', - expect: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly', - NodeJS: 'readonly', - global: 'readonly', - }, - }, - plugins: { - '@typescript-eslint': typescript, - }, - rules: { - // Disable base no-unused-vars in favor of @typescript-eslint/no-unused-vars - 'no-unused-vars': 'off', - // Package-specific overrides can go here - }, - }, - { - ignores: [ - 'dist/', - 'node_modules/', - '*.js', - 'src/templates/', - 'tests/*.d.ts', - 'tests/*.d.ts.map', - ], - }, -]; diff --git a/packages/swarm-wasp/package.json b/packages/swarm-wasp/package.json index a38b6537..3f07b081 100644 --- a/packages/swarm-wasp/package.json +++ b/packages/swarm-wasp/package.json @@ -17,10 +17,10 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "lint": "eslint src --ext .ts --fix", - "lint:check": "eslint src --ext .ts", - "format": "prettier --write --list-different src", - "format:check": "prettier --check src", + "lint": "biome check --write .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format .", "typecheck": "tsc --project tsconfig.typecheck.json", "validate": "pnpm run build && pnpm run typecheck && pnpm test && pnpm run lint && pnpm run format" }, @@ -51,36 +51,36 @@ "url": "https://github.com/Genyus/swarm/issues" }, "engines": { - "node": ">=22.12.0" + "node": ">=24.14.1" }, "swarm": { - "wasp": ">=0.18.0 <0.22.0" + "wasp": ">=0.24.0 <0.25.0" }, "dependencies": { "@ingenyus/swarm": "workspace:*", "@mrleebo/prisma-ast": "^0.14.0", "eta": "^4.5.1", - "semver": "^7.7.4" + "semver": "^7.7.4", + "typescript": "^5.9.0" }, "peerDependencies": { - "wasp-config": "*", + "@wasp.sh/spec": "*", "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { - "wasp-config": { + "@wasp.sh/spec": { "optional": true } }, "devDependencies": { - "@types/node": "^25.3.0", + "@types/node": "^26.1.1", "@types/semver": "^7.7.1", "@vitest/coverage-v8": "^4.0.18", "glob": "^13.0.6", "rimraf": "^6.1.3", "tsup": "^8.5.1", - "typescript": "^5.9.3", "vitest": "^4.0.18", - "wasp-config": "link:src/wasp-config/stubs", + "@wasp.sh/spec": "link:src/wasp-config/stubs", "zod": "^4.3.6" }, "publishConfig": { diff --git a/packages/swarm-wasp/src/common/constants.types.ts b/packages/swarm-wasp/src/common/constants.types.ts index c0805961..c7b04569 100644 --- a/packages/swarm-wasp/src/common/constants.types.ts +++ b/packages/swarm-wasp/src/common/constants.types.ts @@ -3,9 +3,8 @@ * These types are derived from the constants in constants.ts. */ -import { +import type { ACTION_OPERATIONS, - API_HTTP_METHODS, CONFIG_TYPES, CRUD_OPERATIONS, OPERATION_TYPES, @@ -17,11 +16,6 @@ import { */ export type OperationType = (typeof OPERATION_TYPES)[number]; -/** - * Type for valid API HTTP method values. - */ -export type ApiHttpMethod = (typeof API_HTTP_METHODS)[number]; - /** * Type for valid CRUD operation values. */ diff --git a/packages/swarm-wasp/src/common/filesystem.ts b/packages/swarm-wasp/src/common/filesystem.ts index c56eb3c8..e5e0108b 100644 --- a/packages/swarm-wasp/src/common/filesystem.ts +++ b/packages/swarm-wasp/src/common/filesystem.ts @@ -1,6 +1,6 @@ -import { FileSystem, toPascalCase } from '@ingenyus/swarm'; import fs from 'node:fs'; -import path from 'path'; +import path from 'node:path'; +import { type FileSystem, toPascalCase } from '@ingenyus/swarm'; import { TYPE_DIRECTORIES } from './constants'; export const realFileSystem: FileSystem = { diff --git a/packages/swarm-wasp/src/common/prisma.ts b/packages/swarm-wasp/src/common/prisma.ts index 941c6850..6909edc8 100644 --- a/packages/swarm-wasp/src/common/prisma.ts +++ b/packages/swarm-wasp/src/common/prisma.ts @@ -1,13 +1,13 @@ +import fs from 'node:fs'; +import path from 'node:path'; import { - Field, - getSchema, type Attribute, type AttributeArgument, type Block, + type Field, + getSchema, type Model, } from '@mrleebo/prisma-ast'; -import fs from 'node:fs'; -import path from 'node:path'; /** * Represents a field in a Prisma model. @@ -49,7 +49,7 @@ export async function getEntityMetadata( (m: Block): m is Model => m.type === 'model' && m.name === modelName ); - if (!model || model.type !== 'model') { + if (model?.type !== 'model') { throw new Error(`Model ${modelName} not found in schema`); } diff --git a/packages/swarm-wasp/src/common/templates.ts b/packages/swarm-wasp/src/common/templates.ts index 191d723b..0d93d9a7 100644 --- a/packages/swarm-wasp/src/common/templates.ts +++ b/packages/swarm-wasp/src/common/templates.ts @@ -1,6 +1,6 @@ -import { toKebabCase, type FileSystem } from '@ingenyus/swarm'; +import path from 'node:path'; +import { type FileSystem, toKebabCase } from '@ingenyus/swarm'; import { Eta } from 'eta'; -import path from 'path'; export class TemplateUtility { constructor(private fileSystem: FileSystem) {} diff --git a/packages/swarm-wasp/src/common/wasp-compatibility.ts b/packages/swarm-wasp/src/common/wasp-compatibility.ts index 61ecbe09..3e0710cf 100644 --- a/packages/swarm-wasp/src/common/wasp-compatibility.ts +++ b/packages/swarm-wasp/src/common/wasp-compatibility.ts @@ -1,7 +1,7 @@ -import { Logger, findPackageJson, getVersion } from '@ingenyus/swarm'; import { execSync } from 'node:child_process'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { findPackageJson, getVersion, type Logger } from '@ingenyus/swarm'; import * as semver from 'semver'; let cachedSupportedRange: string | null = null; @@ -38,7 +38,7 @@ function getWaspSupportedRange(): string { throw new Error( 'Wasp supported version range not found in package.json. ' + 'Please specify swarm.wasp field in @ingenyus/swarm-wasp package.json ' + - `(e.g., "swarm": { "wasp": ">=0.18.0 <0.20.0" }).` + `(e.g., "swarm": { "wasp": ">=0.24.0 <0.25.0" }).` ); } @@ -83,10 +83,11 @@ function getInstalledWaspVersion(logger: Logger): string { } return firstLine; - } catch (error: any) { - if (error.code === 'ENOENT' || error.message?.includes('wasp')) { + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err.code === 'ENOENT' || err.message?.includes('wasp')) { logger.error( - 'Wasp CLI not found. Install using: curl -sSL https://get.wasp.sh/installer.sh | sh -s' + 'Wasp CLI not found. Install using: npm install -g @wasp.sh/wasp-cli@latest' ); throw new Error('Wasp CLI not found'); diff --git a/packages/swarm-wasp/src/generators/action/action-generator.test.ts b/packages/swarm-wasp/src/generators/action/action-generator.test.ts index 5be89437..1df9185c 100644 --- a/packages/swarm-wasp/src/generators/action/action-generator.test.ts +++ b/packages/swarm-wasp/src/generators/action/action-generator.test.ts @@ -1,665 +1,35 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { ActionGenerator } from './action-generator'; import { schema } from './schema'; -vi.mock('../../common', async () => { - const actual = await vi.importActual('../../common'); - return { - ...actual, - getEntityMetadata: vi.fn(), - getIdFields: vi.fn(), - getRequiredFields: vi.fn(), - getOptionalFields: vi.fn(), - getJsonFields: vi.fn(), - needsPrismaImport: vi.fn(), - generateJsonTypeHandling: vi.fn(), - generatePickType: vi.fn(), - generateOmitType: vi.fn(), - generatePartialType: vi.fn(), - generateIntersectionType: vi.fn(), - normaliseFeaturePath: vi.fn((path) => path), - getFeatureDir: vi.fn((fs, path) => `/test-project/src/features/${path}`), - getFeatureImportPath: vi.fn((path) => path), - ensureDirectoryExists: vi.fn(), - TemplateUtility: function MockTemplateUtility(this: any) { - this.processTemplate = vi.fn(); - this.resolveTemplatePath = vi.fn(); - }, - }; -}); - -vi.mock('../../common/prisma', async () => { - const actual = await vi.importActual('../../common/prisma'); - return { - ...actual, - getEntityMetadata: vi.fn(), - getIdFields: vi.fn(), - getRequiredFields: vi.fn(), - getOptionalFields: vi.fn(), - getJsonFields: vi.fn(), - needsPrismaImport: vi.fn(), - generateJsonTypeHandling: vi.fn(), - generatePickType: vi.fn(), - generateOmitType: vi.fn(), - generatePartialType: vi.fn(), - generateIntersectionType: vi.fn(), - }; -}); - describe('ActionGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: ActionGenerator; beforeEach(async () => { - const { - getEntityMetadata, - getIdFields, - getRequiredFields, - getOptionalFields, - getJsonFields, - needsPrismaImport, - generateJsonTypeHandling, - generatePickType, - generateOmitType, - generatePartialType, - generateIntersectionType, - } = await import('../../common'); - - (getEntityMetadata as any).mockResolvedValue({ - name: 'User', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'email', type: 'String', tsType: 'string', isRequired: true }, - { name: 'name', type: 'String', tsType: 'string', isRequired: false }, - ], - }); - (getIdFields as any).mockReturnValue(['id']); - (getRequiredFields as any).mockReturnValue(['email']); - (getOptionalFields as any).mockReturnValue(['name']); - (getJsonFields as any).mockReturnValue([]); - (needsPrismaImport as any).mockReturnValue(false); - (generateJsonTypeHandling as any).mockReturnValue(''); - (generatePickType as any).mockImplementation( - (modelName: string, fields: string[]) => - fields.length - ? `Pick<${modelName}, ${fields.map((f) => `"${f}"`).join(' | ')}>` - : '' - ); - (generateOmitType as any).mockImplementation( - (modelName: string, fields: string[]) => - fields.length - ? `Omit<${modelName}, ${fields.map((f) => `"${f}"`).join(' | ')}>` - : modelName - ); - (generatePartialType as any).mockImplementation((typeString: string) => - typeString ? `Partial<${typeString}>` : '' - ); - (generateIntersectionType as any).mockImplementation( - (type1: string, type2: string) => { - if (!type1 && !type2) return ''; - if (!type1) return type2; - if (!type2) return type1; - return `${type1} & ${type2}`; - } - ); - - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - }); - - it('generate writes action file and updates config', async () => { - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; // config file exists - if (typeof p === 'string' && p.endsWith('.ts')) return false; // action file does not exist - if (typeof p === 'string' && p.includes('actions')) return false; // action dir does not exist - if (typeof p === 'string' && p.includes('schema.prisma')) return true; // Prisma schema exists - return true; // all others exist - }); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('schema.prisma')) { - return `model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -}`; - } - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - fs.copyFileSync = vi.fn(); - fs.mkdirSync = vi.fn(); - - // Create generator after setting up mocks - gen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - // Mock the template utility to return a simple template - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Config template - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addAction(feature, "${replacements.operationName}", { - entities: [], - auth: false, - })`; - } - // Operation template - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }), - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/action/templates/create.eta`; - } - ), - }; - - await gen.generate({ - feature: 'foo', - dataType: 'User', - operation: 'create', - entities: ['User'], - force: true, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - // The WaspBaseGenerator uses its own configGenerator instead of updateFeatureConfig - // So we expect the config file to be written directly - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('feature.wasp.ts'), - expect.any(String) - ); - }); - - it('handles update action', async () => { - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('actions')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('schema.prisma')) { - return `model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -}`; - } - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - gen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Config template - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addAction(feature, "${replacements.operationName}", { - entities: [], - auth: false, - })`; - } - // Operation template - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }), - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/action/templates/update.eta`; - } - ), - }; - - await gen.generate({ - feature: 'bar', - dataType: 'User', - operation: 'update', - entities: ['User'], - force: true, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - }); - - it('handles delete action', async () => { - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('actions')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('schema.prisma')) { - return `model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -}`; - } - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - gen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Config template - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addAction(feature, "${replacements.operationName}", { - entities: [], - auth: false, - })`; - } - // Operation template - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }), - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/action/templates/delete.eta`; - } - ), - }; - - await gen.generate({ - feature: 'baz', - dataType: 'User', - operation: 'delete', - entities: ['User'], - force: true, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - }); - - it('automatically includes dataType in entities array when not specified', async () => { - const { getEntityMetadata } = await import('../../common/prisma'); - (getEntityMetadata as any).mockResolvedValue({ - name: 'Task', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'name', type: 'String', tsType: 'string', isRequired: true }, - ], - }); - - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('actions')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - const testGen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addAction(feature, "${replacements.operationName}", { - entities: ${replacements.entities}, - auth: false, - })`; - } - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }); - - (testGen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/action/templates/create.eta`; - } - ), - }; - - await testGen.generate({ - feature: 'tasks', - dataType: 'Task', - operation: 'create', - force: true, - }); - - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - entities: '"Task"', - }) - ); - }); - - it('prevents duplicate dataType in entities array', async () => { - const { getEntityMetadata } = await import('../../common'); - (getEntityMetadata as any).mockResolvedValue({ - name: 'Task', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'name', type: 'String', tsType: 'string', isRequired: true }, - ], - }); - - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('actions')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - const testGen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addAction(feature, "${replacements.operationName}", { - entities: ${replacements.entities}, - auth: false, - })`; - } - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }); - - (testGen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/action/templates/create.eta`; - } - ), - }; - - await testGen.generate({ - feature: 'tasks', - dataType: 'Task', - operation: 'create', - entities: ['Task'], - force: true, - }); - - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - entities: '"Task"', - }) - ); + gen = await createTestGenerator(ActionGenerator, schema); }); - it('places dataType first in entities array with other entities', async () => { - const { getEntityMetadata } = await import('../../common'); - (getEntityMetadata as any).mockResolvedValue({ - name: 'Task', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'name', type: 'String', tsType: 'string', isRequired: true }, - ], - }); - - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('actions')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - const testGen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addAction(feature, "${replacements.operationName}", { - entities: ${replacements.entities}, - auth: false, - })`; - } - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }); - - (testGen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/action/templates/create.eta`; - } - ), - }; - - await testGen.generate({ - feature: 'tasks', - dataType: 'Task', - operation: 'create', - entities: ['User', 'Product'], - force: true, - }); - - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - entities: '"Task", "User", "Product"', - }) + it('getOperationDefinition builds a native action declaration', () => { + const decl = gen.getOperationDefinition( + 'createTask', + ['Task'], + 'action', + true ); - }); - - it('getDefinition throws error when config template file does not exist', async () => { - gen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - // Mock resolveTemplatePath to return a non-existent path - const nonExistentPath = '/non/existent/path/operation.eta'; - (gen as any).templateUtility = { - resolveTemplatePath: vi.fn(() => nonExistentPath), - processTemplate: vi.fn((templatePath) => { - // Simulate file not found error - Eta will throw if template doesn't exist - throw new Error(`Template file not found: ${templatePath}`); - }), - }; - // Mock fs.existsSync to return false for the template path - fs.existsSync = vi.fn((path) => { - if (path === nonExistentPath) { - return false; - } - return true; - }); - - expect(() => { - gen.getDefinition( - 'testAction', - 'test', - ['User'], - 'action', - 'features/test/server/actions/testAction', - false - ); - }).toThrow(/Template file not found/); - - // Verify resolveTemplatePath was called with correct parameters - expect( - (gen as any).templateUtility.resolveTemplatePath - ).toHaveBeenCalledWith( - 'operation.eta', - 'config', - expect.any(String) // import.meta.url + expect(decl.kind).toBe('action'); + expect(decl.call).toBe( + 'action(createTask, { entities: ["Task"], auth: true })' ); + expect(decl.refImports).toEqual([ + { names: ['createTask'], from: './server/actions/createTask' }, + ]); }); - it('getDefinition verifies config template path resolution', async () => { - gen = await createTestGenerator(ActionGenerator, schema, { - fileSystem: fs, - }); - - const mockResolveTemplatePath = vi.fn((templateName, generatorName) => { - if (templateName === 'operation.eta' && generatorName === 'config') { - return '/mock/templates/config/operation.eta'; - } - return `/mock/templates/${templateName}`; - }); - - (gen as any).templateUtility = { - resolveTemplatePath: mockResolveTemplatePath, - processTemplate: vi.fn(() => 'app.addAction("test", {});'), - }; - - fs.existsSync = vi.fn(() => true); + it('omits entities when none are given', () => { + const decl = gen.getOperationDefinition('doThing', [], 'action', false); - await gen.getDefinition( - 'testAction', - 'test', - ['User'], - 'action', - 'features/test/server/actions/testAction', - false - ); - - // Verify resolveTemplatePath was called with correct parameters for config template - expect(mockResolveTemplatePath).toHaveBeenCalledWith( - 'operation.eta', - 'config', - expect.any(String) // import.meta.url - ); + expect(decl.call).toBe('action(doThing, { auth: false })'); }); }); diff --git a/packages/swarm-wasp/src/generators/action/action-generator.ts b/packages/swarm-wasp/src/generators/action/action-generator.ts index 766e8d0d..5199eabf 100644 --- a/packages/swarm-wasp/src/generators/action/action-generator.ts +++ b/packages/swarm-wasp/src/generators/action/action-generator.ts @@ -1,4 +1,4 @@ -import { GeneratorServices, Out } from '@ingenyus/swarm'; +import type { Out } from '@ingenyus/swarm'; import { CONFIG_TYPES } from '../../common'; import { OperationGeneratorBase } from '../base'; import { schema } from './schema'; @@ -14,10 +14,6 @@ export class ActionGenerator extends OperationGeneratorBase< description = 'Generates a Wasp Action'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const { dataType, feature, name } = args; const operation = args.operation; @@ -46,9 +42,10 @@ export class ActionGenerator extends OperationGeneratorBase< this.ensureWaspCompatible(); const configPath = this.validateFeatureConfig(feature); - const { targetDirectory: operationsDir, importDirectory } = - this.ensureTargetDirectory(feature, operationType); - const importPath = `${importDirectory}/${operationName}`; + const { targetDirectory: operationsDir } = this.ensureTargetDirectory( + feature, + operationType + ); this.generateOperationFile( operationsDir, @@ -58,19 +55,15 @@ export class ActionGenerator extends OperationGeneratorBase< ); // Generate config definition and update - const definition = this.getDefinition( + const definition = this.getOperationDefinition( operationName, - feature, entities, 'action', - importPath, args.auth ); this.updateConfigWithCheck( configPath, - 'addAction', - operationName, definition, feature, args.force || false diff --git a/packages/swarm-wasp/src/generators/action/schema.ts b/packages/swarm-wasp/src/generators/action/schema.ts index 28124dd7..70b5b27c 100644 --- a/packages/swarm-wasp/src/generators/action/schema.ts +++ b/packages/swarm-wasp/src/generators/action/schema.ts @@ -1,4 +1,4 @@ -import { SchemaManager, registerSchemaMetadata } from '@ingenyus/swarm'; +import { registerSchemaMetadata, SchemaManager } from '@ingenyus/swarm'; import { z } from 'zod/v4'; import { ACTION_OPERATIONS, diff --git a/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.test.ts b/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.test.ts index 5e2b7d9a..5d098caa 100644 --- a/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.test.ts +++ b/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.test.ts @@ -1,118 +1,22 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { ApiNamespaceGenerator } from './api-namespace-generator'; import { schema } from './schema'; describe('ApiNamespaceGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: ApiNamespaceGenerator; beforeEach(async () => { - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(ApiNamespaceGenerator, schema, { - fileSystem: fs, - }); + gen = await createTestGenerator(ApiNamespaceGenerator, schema); }); - it('generate writes middleware file and updates config', async () => { - fs.existsSync = vi.fn((p) => !p.includes('notfound')); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; + it('getDefinition builds a native apiNamespace declaration', () => { + const decl = gen.getDefinition('bar', '/bar'); -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'template'; - }); - fs.writeFileSync = vi.fn(); - - // Mock the path methods to return predictable paths - const mockPathJoin = vi.fn((...args) => args.join('/')); - const mockPathDirname = vi.fn((path) => - path.split('/').slice(0, -1).join('/') - ); - (gen as any).path = { join: mockPathJoin, dirname: mockPathDirname }; - - // Mock the getTemplatePath method to return a predictable path - const mockGetTemplatePath = vi.fn((templateName) => { - return `/mock/templates/${templateName}`; - }); - (gen as any).getTemplatePath = mockGetTemplatePath; - - // Mock the template utility to return a simple template - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - if (templatePath.includes('middleware.eta')) { - return `// Generated middleware template for ${replacements.name || 'unknown'}`; - } else if (templatePath.includes('config/apiNamespace.eta')) { - return `app.addApiNamespace("${replacements.namespaceName}", { - middleware: "${replacements.middlewareImportPath}", - path: "${replacements.pathValue}" -});`; - } - return `// Generated template for ${replacements.name || 'unknown'}`; - }), - resolveTemplatePath: vi.fn((templateName) => { - return `/mock/templates/shared/${templateName}`; - }), - }; - - // Mock the getDefinition method to return a proper config definition - const mockGetDefinition = vi.fn(() => { - return `app.addApiNamespace("ns", { - middleware: "features/foo/server/middleware/ns", - path: "/api" -});`; - }); - (gen as any).getDefinition = mockGetDefinition; - - await gen.generate({ - feature: 'foo', - name: 'ns', - path: '/api', - force: true, - }); - expect(fs.writeFileSync).toHaveBeenCalled(); - // The WaspBaseGenerator uses its own configGenerator instead of updateFeatureConfig - // So we expect the config file to be written directly - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('feature.wasp.ts'), - expect.any(String) - ); - }); - - it('getDefinition returns processed template', async () => { - // Mock the template utility - (gen as any).templateUtility = { - processTemplate: vi.fn( - () => - 'app.addApiNamespace("testNamespace", { middleware: import("..."), path: "/api/test" });' - ), - resolveTemplatePath: vi.fn( - (templateName) => `/mock/templates/${templateName}` - ), - }; - - // Mock the getTemplatePathWithOverride method - (gen as any).getTemplatePathWithOverride = vi.fn(() => - Promise.resolve('/mock/templates/config/api-namespace.eta') - ); - - const result = await gen.getDefinition( - 'testNamespace', - 'features/test/server/middleware/testMiddleware', - '/api/test' - ); - expect(typeof result).toBe('string'); + expect(decl.kind).toBe('apiNamespace'); + expect(decl.call).toBe('apiNamespace("/bar", { middlewareConfigFn: bar })'); + expect(decl.refImports).toEqual([ + { names: ['bar'], from: './server/apis/middleware/bar' }, + ]); }); }); diff --git a/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.ts b/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.ts index 6393df01..635eb226 100644 --- a/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.ts +++ b/packages/swarm-wasp/src/generators/api-namespace/api-namespace-generator.ts @@ -1,7 +1,8 @@ -import { GeneratorServices, Out, toCamelCase } from '@ingenyus/swarm'; import path from 'node:path'; +import { type Out, toCamelCase } from '@ingenyus/swarm'; import { CONFIG_TYPES, ensureDirectoryExists } from '../../common'; import { ComponentGeneratorBase } from '../base'; +import type { SpecDeclaration } from '../config'; import { schema } from './schema'; export class ApiNamespaceGenerator extends ComponentGeneratorBase< @@ -15,10 +16,6 @@ export class ApiNamespaceGenerator extends ComponentGeneratorBase< description = 'Generates a Wasp API Namespace'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const { name, path: namespacePath, feature } = args; const namespaceName = toCamelCase(name); @@ -30,10 +27,8 @@ export class ApiNamespaceGenerator extends ComponentGeneratorBase< this.ensureWaspCompatible(); const configPath = this.validateFeatureConfig(feature); - const { - targetDirectory: apiTargetDirectory, - importDirectory: apiImportDirectory, - } = this.ensureTargetDirectory(feature, 'api'); + const { targetDirectory: apiTargetDirectory } = + this.ensureTargetDirectory(feature, 'api'); const middlewareTargetDirectory = path.join( apiTargetDirectory, 'middleware' @@ -41,7 +36,6 @@ export class ApiNamespaceGenerator extends ComponentGeneratorBase< ensureDirectoryExists(this.fileSystem, middlewareTargetDirectory); - const importDirectory = `${apiImportDirectory}/middleware`; const targetFile = `${middlewareTargetDirectory}/${namespaceName}.ts`; await this.generateMiddlewareFile( @@ -49,60 +43,37 @@ export class ApiNamespaceGenerator extends ComponentGeneratorBase< namespaceName, args.force || false ); - await this.updateConfigFile( - namespaceName, - importDirectory, - namespacePath, - args, - configPath - ); + this.updateConfigFile(namespaceName, namespacePath, args, configPath); } ); } - private async updateConfigFile( + private updateConfigFile( namespaceName: string, - importDirectory: string, namespacePath: string, args: Out, configFilePath: string ) { - const { force = false } = args; - const importPath = `${importDirectory}/${namespaceName}`; - const definition = await this.getDefinition( - namespaceName, - importPath, - namespacePath - ); + const definition = this.getDefinition(namespaceName, namespacePath); this.updateConfigWithCheck( configFilePath, - 'addApiNamespace', - namespaceName, definition, args.feature, - force + args.force || false ); } /** - * Generates an apiNamespace definition for the feature configuration. + * Builds a native apiNamespace spec declaration for the feature configuration. */ - async getDefinition( - namespaceName: string, - middlewareImportPath: string, - pathValue: string - ): Promise { - const templatePath = this.templateUtility.resolveTemplatePath( - 'config/api-namespace.eta', - 'api-namespace', - import.meta.url - ); + getDefinition(namespaceName: string, pathValue: string): SpecDeclaration { + const from = this.getRelativeRefPath('api', `middleware/${namespaceName}`); - return this.templateUtility.processTemplate(templatePath, { - namespaceName, - middlewareImportPath, - pathValue, - }); + return { + kind: 'apiNamespace', + call: `apiNamespace("${pathValue}", { middlewareConfigFn: ${namespaceName} })`, + refImports: [{ names: [namespaceName], from }], + }; } } diff --git a/packages/swarm-wasp/src/generators/api-namespace/templates/config/api-namespace.eta b/packages/swarm-wasp/src/generators/api-namespace/templates/config/api-namespace.eta deleted file mode 100644 index a2b8835e..00000000 --- a/packages/swarm-wasp/src/generators/api-namespace/templates/config/api-namespace.eta +++ /dev/null @@ -1,3 +0,0 @@ - .addApiNamespace(feature, "<%=namespaceName%>", { - path: "<%=pathValue%>", - }) diff --git a/packages/swarm-wasp/src/generators/api/api-generator.test.ts b/packages/swarm-wasp/src/generators/api/api-generator.test.ts index 9b9c8b62..d362754a 100644 --- a/packages/swarm-wasp/src/generators/api/api-generator.test.ts +++ b/packages/swarm-wasp/src/generators/api/api-generator.test.ts @@ -1,288 +1,44 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { DEFAULT_CUSTOM_TEMPLATES_DIR } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { ApiGenerator } from './api-generator'; import { schema } from './schema'; -// Mock getConfigManager -vi.mock('@ingenyus/swarm', async () => { - const actual = await vi.importActual('@ingenyus/swarm'); - return { - ...actual, - getConfigManager: vi.fn().mockImplementation(() => ({ - loadConfig: vi.fn().mockResolvedValue({ - templateDirectory: DEFAULT_CUSTOM_TEMPLATES_DIR, - plugins: [ - { - from: '@ingenyus/swarm-wasp', - import: 'wasp', - }, - ], - }), - })), - }; -}); - describe('ApiGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: ApiGenerator; beforeEach(async () => { - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(ApiGenerator, schema, { - fileSystem: fs, - }); - }); - - it('generate writes handler and updates config', async () => { - // Mock existsSync to return true for all paths (including config files) - fs.existsSync = vi.fn(() => true); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'template'; - }); - fs.writeFileSync = vi.fn(); - - // Mock the template utility to return proper templates - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Return different content based on the template path - if (templatePath.includes('config/api.eta')) { - // Return a valid config definition that can be parsed - return `app.addApi("${replacements.apiName}", { - method: ${replacements.method}, - route: "${replacements.route}", - handler: "${replacements.importPath}" -});`; - } - // Default API handler template - return `// Generated API handler for ${replacements.apiName || 'unknown'}`; - }), - resolveTemplatePath: vi.fn( - (templateName) => `/mock/templates/${templateName}` - ), - }; - - await gen.generate({ - feature: 'foo', - name: 'api', - method: 'GET', - path: '/api', - force: true, - entities: ['User'], - auth: true, - customMiddleware: false, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - // The WaspBaseGenerator uses its own configGenerator instead of updateFeatureConfig - // So we expect the config file to be written directly - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('feature.wasp.ts'), - expect.any(String) - ); - }); - - it('generate writes middleware file when customMiddleware is true', async () => { - // Mock existsSync to return true for config files, false for middleware directory - fs.existsSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('middleware')) { - return false; // Middleware directory doesn't exist yet - } - return true; // Config files and other paths exist - }); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'template'; - }); - fs.writeFileSync = vi.fn(); - fs.mkdirSync = vi.fn(); - - // Mock the path methods to return predictable paths - const mockPathJoin = vi.fn((...args) => args.join('/')); - const mockPathDirname = vi.fn((path) => - path.split('/').slice(0, -1).join('/') - ); - (gen as any).path = { join: mockPathJoin, dirname: mockPathDirname }; - - const mockResolveTemplatePath = vi.fn( - (templateName) => `/mock/templates/shared/${templateName}` - ); - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if (templatePath.includes('middleware.eta')) { - return `// Generated middleware template for ${replacements.name || 'unknown'}`; - } else if (templatePath.includes('config/api.eta')) { - return `app.addApi("${replacements.apiName}", { - method: ${replacements.method}, - route: "${replacements.route}", - handler: "${replacements.importPath}" -});`; - } - // Default API handler template - return `// Generated API handler for ${replacements.apiName || 'unknown'}`; - }); - - // Mock the template utility - (gen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: mockResolveTemplatePath, - }; - - await gen.generate({ - feature: 'foo', - name: 'testApi', - method: 'GET', - path: '/api', - force: true, - entities: ['User'], - auth: true, - customMiddleware: true, - }); - - // Verify middleware directory was created - expect(fs.mkdirSync).toHaveBeenCalledWith( - expect.stringContaining('middleware'), - expect.objectContaining({ recursive: true }) - ); - - // Verify middleware file was written - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('middleware/testApi.ts'), - expect.stringContaining('Generated middleware template for testApi') - ); - - // Verify resolveTemplatePath was called with correct middleware template path - expect(mockResolveTemplatePath).toHaveBeenCalledWith( - 'middleware/middleware.eta', - 'shared', - expect.any(String) // import.meta.url - ); - - // Verify processTemplate was called with middleware template - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.stringContaining('middleware.eta'), - expect.objectContaining({ name: 'testApi' }) - ); + gen = await createTestGenerator(ApiGenerator, schema); }); - it('generate does not write middleware file when customMiddleware is false', async () => { - // Mock existsSync to return true for all paths (including config files) - fs.existsSync = vi.fn(() => true); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'template'; - }); - fs.writeFileSync = vi.fn(); - fs.mkdirSync = vi.fn(); - - // Mock the path methods - const mockPathJoin = vi.fn((...args) => args.join('/')); - const mockPathDirname = vi.fn((path) => - path.split('/').slice(0, -1).join('/') - ); - (gen as any).path = { join: mockPathJoin, dirname: mockPathDirname }; - - const mockResolveTemplatePath = vi.fn( - (templateName) => `/mock/templates/${templateName}` - ); - - // Mock the template utility - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - if (templatePath.includes('config/api.eta')) { - return `app.addApi("${replacements.apiName}", { - method: ${replacements.method}, - route: "${replacements.route}", - handler: "${replacements.importPath}" -});`; - } - return `// Generated API handler for ${replacements.apiName || 'unknown'}`; - }), - resolveTemplatePath: mockResolveTemplatePath, - }; - - await gen.generate({ - feature: 'foo', - name: 'testApi', - method: 'GET', - path: '/api', - force: true, - entities: ['User'], - auth: true, - customMiddleware: false, - }); - - // Verify middleware directory was NOT created - expect(fs.mkdirSync).not.toHaveBeenCalledWith( - expect.stringContaining('middleware'), - expect.any(Object) - ); - - // Verify resolveTemplatePath was NOT called with middleware template - expect(mockResolveTemplatePath).not.toHaveBeenCalledWith( - 'middleware/middleware.eta', - 'shared', - expect.any(String) + it('getDefinition builds a native api declaration', () => { + const decl = gen.getDefinition( + 'getFoo', + 'GET', + '/foo', + ['Task'], + false, + false ); - // Verify middleware file was NOT written - const writeCalls = (fs.writeFileSync as any).mock.calls; - const middlewareWriteCalls = writeCalls.filter((call: any[]) => - call[0]?.includes('middleware') + expect(decl.kind).toBe('api'); + expect(decl.call).toBe( + 'api("GET", "/foo", getFoo, { entities: ["Task"], auth: false })' ); - expect(middlewareWriteCalls).toHaveLength(0); + expect(decl.refImports).toEqual([ + { names: ['getFoo'], from: './server/apis/getFoo' }, + ]); }); - it('getConfigDefinition returns processed template', async () => { - // Mock the template utility - (gen as any).templateUtility = { - processTemplate: vi.fn(() => 'app.addApi("testApi", { method: "GET" });'), - resolveTemplatePath: vi.fn( - (templateName) => `/mock/templates/${templateName}` - ), - }; - - // Mock the getTemplatePathWithOverride method - (gen as any).getTemplatePathWithOverride = vi.fn(() => - Promise.resolve('/mock/templates/config/api.eta') - ); + it('adds a distinctly-named middleware ref when customMiddleware is set', () => { + const decl = gen.getDefinition('getFoo', 'POST', '/foo', [], true, true); - const result = await (gen as any).getConfigDefinition( - 'testApi', - 'test', - ['User'], - 'GET', - '/api/test', - 'features/test/server/apis/test', - false, - 'features/test/server/apis/test' + expect(decl.call).toBe( + 'api("POST", "/foo", getFoo, { middlewareConfigFn: getFooMiddleware, auth: true })' ); - expect(typeof result).toBe('string'); + // The middleware export is suffixed to avoid colliding with the api handler. + expect(decl.refImports).toEqual([ + { names: ['getFoo'], from: './server/apis/getFoo' }, + { names: ['getFooMiddleware'], from: './server/apis/middleware/getFoo' }, + ]); }); }); diff --git a/packages/swarm-wasp/src/generators/api/api-generator.ts b/packages/swarm-wasp/src/generators/api/api-generator.ts index 1c235be2..6d942fba 100644 --- a/packages/swarm-wasp/src/generators/api/api-generator.ts +++ b/packages/swarm-wasp/src/generators/api/api-generator.ts @@ -1,15 +1,7 @@ -import { - GeneratorServices, - Out, - toCamelCase, - toPascalCase, -} from '@ingenyus/swarm'; -import { - CONFIG_TYPES, - ensureDirectoryExists, - getFeatureImportPath, -} from '../../common'; +import { type Out, toCamelCase, toPascalCase } from '@ingenyus/swarm'; +import { CONFIG_TYPES, ensureDirectoryExists } from '../../common'; import { ComponentGeneratorBase } from '../base'; +import type { SpecDeclaration } from '../config'; import { schema } from './schema'; export class ApiGenerator extends ComponentGeneratorBase< @@ -23,10 +15,6 @@ export class ApiGenerator extends ComponentGeneratorBase< description = 'Generates a Wasp API Endpoint'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const apiName = toCamelCase(args.name); @@ -34,10 +22,8 @@ export class ApiGenerator extends ComponentGeneratorBase< this.ensureWaspCompatible(); const configPath = this.validateFeatureConfig(args.feature); - const { - targetDirectory: apiTargetDirectory, - importDirectory: apiImportDirectory, - } = this.ensureTargetDirectory(args.feature, this.name); + const { targetDirectory: apiTargetDirectory } = + this.ensureTargetDirectory(args.feature, this.name); const fileName = `${apiName}.ts`; const targetFile = `${apiTargetDirectory}/${fileName}`; @@ -55,18 +41,12 @@ export class ApiGenerator extends ComponentGeneratorBase< this.generateMiddlewareFile( middlewareFile, - apiName, + `${apiName}Middleware`, args.force || false ); } - await this.updateConfigFile( - apiName, - fileName, - apiImportDirectory, - args, - configPath - ); + this.updateConfigFile(apiName, args, configPath); }); } @@ -86,70 +66,61 @@ export class ApiGenerator extends ComponentGeneratorBase< ); } - private async updateConfigFile( + private updateConfigFile( apiName: string, - apiFile: string, - importDirectory: string, args: Out, configFilePath: string ) { - const { - feature, - force = false, - entities, - method, - path, - auth, - customMiddleware, - } = args; - const importPath = this.path.join(importDirectory, apiFile); - const definition = await this.getConfigDefinition( + const { feature, force = false, entities, method, path, auth } = args; + const entityList = Array.isArray(entities) + ? entities + : entities + ? [entities] + : []; + const definition = this.getDefinition( apiName, - feature, - Array.isArray(entities) ? entities : entities ? [entities] : [], method, path, - apiFile, - auth, - importPath, - customMiddleware || false + entityList, + auth || false, + args.customMiddleware || false ); - this.updateConfigWithCheck( - configFilePath, - 'addApi', - apiName, - definition, - feature, - force - ); + this.updateConfigWithCheck(configFilePath, definition, feature, force); } - private async getConfigDefinition( + /** + * Builds a native api spec declaration for the feature configuration. + */ + getDefinition( apiName: string, - featurePath: string, - entities: string[], method: string, route: string, - apiFile: string, - auth = false, - importPath: string, - customMiddleware = false - ): Promise { - const featureDir = getFeatureImportPath(featurePath); - const configTemplatePath = await this.getTemplatePath('config/api.eta'); - - return this.templateUtility.processTemplate(configTemplatePath, { - apiName, - featureDir, - entities: entities.map((e) => `"${e}"`).join(', '), - method, - route, - apiFile, - auth: String(auth), - importPath, - customMiddleware: String(customMiddleware), - }); + entities: string[], + auth: boolean, + customMiddleware: boolean + ): SpecDeclaration { + const refImports = [ + { names: [apiName], from: this.getRelativeRefPath('api', apiName) }, + ]; + + let middleware: string | undefined; + if (customMiddleware) { + const middlewareName = `${apiName}Middleware`; + middleware = `middlewareConfigFn: ${middlewareName}`; + refImports.push({ + names: [middlewareName], + from: this.getRelativeRefPath('api', `middleware/${apiName}`), + }); + } + + const call = `api("${method}", "${route}", ${apiName}, ${this.configObject([ + middleware, + entities.length ? `entities: ${this.stringArray(entities)}` : undefined, + `auth: ${String(auth)}`, + ])})`; + + return { kind: 'api', call, refImports }; } private buildTemplateData(apiName: string, method: string, auth: boolean) { diff --git a/packages/swarm-wasp/src/generators/api/templates/config/api.eta b/packages/swarm-wasp/src/generators/api/templates/config/api.eta deleted file mode 100644 index d21cf494..00000000 --- a/packages/swarm-wasp/src/generators/api/templates/config/api.eta +++ /dev/null @@ -1,11 +0,0 @@ - .addApi(feature, "<%=apiName%>", { - method: "<%=method%>", - route: "<%=route%>", -<%- if (entities.length > 0) { %> - entities: [<%=entities%>], -<%- } %> - auth: <%=auth%>, -<%- if (customMiddleware) { %> - customMiddleware: true, -<%- } %> - }) diff --git a/packages/swarm-wasp/src/generators/base/component-generator.base.ts b/packages/swarm-wasp/src/generators/base/component-generator.base.ts index 374f1819..043897de 100644 --- a/packages/swarm-wasp/src/generators/base/component-generator.base.ts +++ b/packages/swarm-wasp/src/generators/base/component-generator.base.ts @@ -1,21 +1,22 @@ +import path from 'node:path'; import { - Generator, - GeneratorServices, - hasHelperMethodCall, - StandardSchemaV1, + capitalise, + type Generator, + type GeneratorServices, + type StandardSchemaV1, toKebabCase, } from '@ingenyus/swarm'; -import path from 'node:path'; import { - ConfigType, + type ConfigType, ensureDirectoryExists, getFeatureDir, normaliseFeaturePath, TYPE_DIRECTORIES, validateFeaturePath, } from '../../common'; +import type { SpecDeclaration } from '../config'; import { FeatureGenerator } from '../feature/feature-generator'; -import { schema as featureSchema } from '../feature/schema'; +import type { schema as featureSchema } from '../feature/schema'; import { WaspGeneratorBase } from './wasp-generator.base'; /** @@ -57,7 +58,7 @@ export abstract class ComponentGeneratorBase< for (let i = segments.length; i > 0; i--) { const pathSegments = segments.slice(0, i); const currentPath = pathSegments.join('/'); - const featureName = pathSegments[pathSegments.length - 1]; + const _featureName = pathSegments[pathSegments.length - 1]; const featureDir = getFeatureDir(this.fileSystem, currentPath); const configPath = path.join(featureDir, `feature.wasp.ts`); @@ -78,73 +79,47 @@ export abstract class ComponentGeneratorBase< } /** - * Checks if a config item already exists in the feature config + * Inserts or replaces a native spec declaration in the feature config, + * erroring if it already exists unless `force` is set. */ - protected checkConfigExists( + protected updateConfigWithCheck( configPath: string, - methodName: string, - itemName: string, + declaration: SpecDeclaration, + featurePath: string, force: boolean - ): boolean { - const configContent = this.fileSystem.readFileSync(configPath, 'utf8'); - const configExists = hasHelperMethodCall( - configContent, - methodName, - itemName - ); - - return this.checkExistence( - configExists, - `${methodName} config already exists in ${configPath}`, + ): void { + const exists = this.checkExistence( + this.configGenerator.has(featurePath, declaration), + `${capitalise(declaration.kind)} config already exists in ${configPath}`, force, - `${methodName} config already exists` + `${declaration.kind} config already exists` ); - } - /** - * Updates the feature config with a new definition - */ - protected updateFeatureConfig( - featurePath: string, - definition: string, - configPath: string, - configExists: boolean, - methodName: string - ): void { - this.configGenerator.update(featurePath, definition); + this.configGenerator.update(featurePath, declaration); this.logger.success( - `${configExists ? 'Updated' : 'Added'} ${methodName} config in: ${configPath}` + `${exists ? 'Updated' : 'Added'} ${declaration.kind} config in: ${configPath}` ); } /** - * Consolidated helper for updating config files with existence check - * This replaces the duplicated updateConfigFile pattern in concrete generators + * Builds a feature-relative `with { type: "ref" }` import path for a component + * of the given type, e.g. `getRelativeRefPath('query', 'getTasks')` -> + * `./server/queries/getTasks`. */ - protected updateConfigWithCheck( - configPath: string, - methodName: string, - entityName: string, - definition: string, - featurePath: string, - force: boolean - ): void { - const configExists = this.checkConfigExists( - configPath, - methodName, - entityName, - force - ); + protected getRelativeRefPath(type: string, name: string): string { + return `./${TYPE_DIRECTORIES[type.toLowerCase()]}/${name}`; + } - if (!configExists || force) { - this.updateFeatureConfig( - featurePath, - definition, - configPath, - configExists, - methodName - ); - } + /** Renders an object literal from `key: value` parts, e.g. `{ auth: true }`. */ + protected configObject(parts: Array): string { + const present = parts.filter((part): part is string => part !== undefined); + + return present.length ? `{ ${present.join(', ')} }` : '{}'; + } + + /** Renders a string array literal, e.g. `["Task", "Tag"]`. */ + protected stringArray(items: string[]): string { + return `[${items.map((item) => `"${item}"`).join(', ')}]`; } /** diff --git a/packages/swarm-wasp/src/generators/base/operation-generator.base.ts b/packages/swarm-wasp/src/generators/base/operation-generator.base.ts index 5a92d913..34b58c03 100644 --- a/packages/swarm-wasp/src/generators/base/operation-generator.base.ts +++ b/packages/swarm-wasp/src/generators/base/operation-generator.base.ts @@ -1,33 +1,30 @@ import { capitalise, getPlural, - handleFatalError, - StandardSchemaV1, + type StandardSchemaV1, toPascalCase, } from '@ingenyus/swarm'; import { - ActionOperation, - CONFIG_TYPES, + type ActionOperation, + type CONFIG_TYPES, copyDirectory, - EntityMetadata, + type EntityMetadata, generateIntersectionType, generateJsonTypeHandling, generateOmitType, generatePartialType, generatePickType, getEntityMetadata, - getFeatureImportPath, getIdFields, getJsonFields, getOptionalFields, getRequiredFields, needsPrismaImport, - OPERATION_TYPES, OPERATIONS, - OperationType, - QueryOperation, - TYPE_DIRECTORIES, + type OperationType, + type QueryOperation, } from '../../common'; +import type { SpecDeclaration } from '../config'; import { ComponentGeneratorBase } from './component-generator.base'; /** @@ -357,37 +354,24 @@ export abstract class OperationGeneratorBase< } /** - * Generates an operation definition for the feature configuration. + * Builds a native query/action spec declaration for the feature configuration. */ - getDefinition( + getOperationDefinition( operationName: string, - featurePath: string, entities: string[], operationType: OperationType, - importPath: string, auth = false - ): string { - if (!OPERATION_TYPES.includes(operationType)) { - handleFatalError(`Unknown operation type: ${operationType}`); - } - const directory = TYPE_DIRECTORIES[operationType]; - const featureDir = getFeatureImportPath(featurePath); - - // Use templateUtility to resolve the config template path - const templatePath = this.templateUtility.resolveTemplatePath( - 'operation.eta', - 'config', - import.meta.url - ); + ): SpecDeclaration { + const from = this.getRelativeRefPath(operationType, operationName); + const call = `${operationType}(${operationName}, ${this.configObject([ + entities.length ? `entities: ${this.stringArray(entities)}` : undefined, + `auth: ${String(auth)}`, + ])})`; - return this.templateUtility.processTemplate(templatePath, { - operationType: capitalise(operationType), - operationName, - featureDir, - directory, - entities: entities.map((e) => `"${e}"`).join(', '), - importPath, - auth: String(auth), - }); + return { + kind: operationType, + call, + refImports: [{ names: [operationName], from }], + }; } } diff --git a/packages/swarm-wasp/src/generators/base/wasp-generator.base.ts b/packages/swarm-wasp/src/generators/base/wasp-generator.base.ts index de5cb909..a89f13fa 100644 --- a/packages/swarm-wasp/src/generators/base/wasp-generator.base.ts +++ b/packages/swarm-wasp/src/generators/base/wasp-generator.base.ts @@ -1,17 +1,17 @@ import { - Config, + type Config, GeneratorBase, - GeneratorServices, - StandardSchemaV1, - TemplateResolver, + type GeneratorServices, getConfigManager, + type StandardSchemaV1, + TemplateResolver, } from '@ingenyus/swarm'; import { + assertWaspCompatible, PLUGIN_NAME, TemplateUtility, - assertWaspCompatible, } from '../../common'; -import { WaspConfigGenerator } from '../config'; +import { FeaturesBarrelGenerator, WaspConfigGenerator } from '../config'; /** * Abstract base class for all Wasp generators @@ -20,6 +20,7 @@ export abstract class WaspGeneratorBase< S extends StandardSchemaV1, > extends GeneratorBase { protected configGenerator: WaspConfigGenerator; + protected barrelGenerator: FeaturesBarrelGenerator; protected templateUtility: TemplateUtility; protected templateResolver: TemplateResolver; private swarmConfig: Config | undefined; @@ -34,6 +35,10 @@ export abstract class WaspGeneratorBase< this.logger, this.fileSystem ); + this.barrelGenerator = new FeaturesBarrelGenerator( + this.logger, + this.fileSystem + ); this.templateUtility = new TemplateUtility(this.fileSystem); this.templateResolver = new TemplateResolver(this.fileSystem); } @@ -100,7 +105,7 @@ export abstract class WaspGeneratorBase< */ protected async renderTemplateToFile( templateName: string, - replacements: Record, + replacements: Record, outputPath: string, readableFileType: string, force: boolean diff --git a/packages/swarm-wasp/src/generators/config/barrel-generator.ts b/packages/swarm-wasp/src/generators/config/barrel-generator.ts new file mode 100644 index 00000000..7d61b942 --- /dev/null +++ b/packages/swarm-wasp/src/generators/config/barrel-generator.ts @@ -0,0 +1,138 @@ +import path from 'node:path'; +import { + type FileSystem, + getCLILogger, + type Logger, + toCamelCase, +} from '@ingenyus/swarm'; +import { findWaspRoot, realFileSystem } from '../../common'; + +const FEATURE_FILE = 'feature.wasp.ts'; +const BARREL_FILE = 'index.wasp.ts'; + +interface DiscoveredFeature { + /** Import specifier relative to the barrel, e.g. `./dashboard/feature.wasp`. */ + importPath: string; + /** Unique identifier alias for the feature's spec, e.g. `dashboardSpec`. */ + alias: string; +} + +/** + * Generates the features barrel (`src/features/index.wasp.ts`). + * + * Wasp 0.24 loads the spec by bundling `main.wasp.ts`'s STATIC import graph, so + * every feature spec must be statically reachable. This barrel statically + * imports each `feature.wasp.ts`'s exported `spec` and nests them into a single + * `featureSpecs` array that `main.wasp.ts` includes in `app({ spec: [...] })`. + * + * It is regenerated whenever a feature is created or removed, giving Swarm's + * auto-discovery ergonomics while satisfying static reachability. + */ +export class FeaturesBarrelGenerator { + constructor( + protected logger: Logger = getCLILogger(), + protected fileSystem: FileSystem = realFileSystem + ) {} + + /** + * Rewrites `src/features/index.wasp.ts` to reflect the feature spec files + * currently present under `src/features`. + */ + regenerate(): void { + const featuresDir = path.join( + findWaspRoot(this.fileSystem), + 'src', + 'features' + ); + + if (!this.fileSystem.existsSync(featuresDir)) { + return; + } + + const features = this.discoverFeatures(featuresDir); + const barrelPath = path.join(featuresDir, BARREL_FILE); + + this.fileSystem.writeFileSync(barrelPath, this.render(features)); + this.logger.success( + `Regenerated features barrel (${features.length} feature${features.length === 1 ? '' : 's'}): ${barrelPath}` + ); + } + + /** + * Recursively finds every `feature.wasp.ts` under `src/features` (features can + * be nested, e.g. `dashboard/features/stats/feature.wasp.ts`). + */ + private discoverFeatures(featuresDir: string): DiscoveredFeature[] { + const found: DiscoveredFeature[] = []; + + const walk = (dir: string): void => { + const entries = this.fileSystem.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + walk(fullPath); + } else if (entry.isFile() && entry.name === FEATURE_FILE) { + const relDir = path + .relative(featuresDir, path.dirname(fullPath)) + .split(path.sep) + .join('/'); + + found.push({ + importPath: `./${relDir}/feature.wasp`, + alias: this.toAlias(relDir), + }); + } + } + }; + + walk(featuresDir); + + // Deterministic, minimal-diff output. + found.sort((a, b) => a.importPath.localeCompare(b.importPath)); + + return found; + } + + /** + * Derives a unique, valid-identifier alias from a feature's relative directory + * path. Nested-feature `features/` segments are dropped so the alias reflects + * the feature chain: `dashboard/features/stats` -> `dashboardStatsSpec`. + */ + private toAlias(relDir: string): string { + const slug = relDir + .split('/') + .filter((segment) => segment && segment !== 'features') + .join('-'); + + const camel = toCamelCase(slug) || 'feature'; + const safe = /^[a-zA-Z_$]/.test(camel) ? camel : `_${camel}`; + + return `${safe}Spec`; + } + + private render(features: DiscoveredFeature[]): string { + const header = [ + '// GENERATED BY SWARM — DO NOT EDIT.', + '// Regenerated whenever features are added or removed. It statically', + '// imports every feature spec so Wasp resolves their references, and nests', + '// them into a single array for `main.wasp.ts` to include in `app({ spec })`.', + "import type { Spec } from '@wasp.sh/spec';", + ]; + + const imports = features.map( + (feature) => + `import { spec as ${feature.alias} } from '${feature.importPath}';` + ); + + const body = + features.length === 0 + ? 'export const featureSpecs: Spec = [];' + : `export const featureSpecs: Spec = [\n${features + .map((feature) => ` ${feature.alias},`) + .join('\n')}\n];`; + + return `${[...header, '', ...imports].join('\n')}\n\n${body}\n`; + } +} diff --git a/packages/swarm-wasp/src/generators/config/config-generator.ts b/packages/swarm-wasp/src/generators/config/config-generator.ts index 9caa3d31..7e975cfb 100644 --- a/packages/swarm-wasp/src/generators/config/config-generator.ts +++ b/packages/swarm-wasp/src/generators/config/config-generator.ts @@ -1,20 +1,46 @@ /** - * Interface for Wasp configuration generators - * ConfigGenerator has its own generate method signature that takes a string argument + * A `with { type: "ref" }` import that a spec declaration depends on. + */ +interface RefImport { + /** Named exports to import, e.g. `["getTasks"]`. */ + names: string[]; + /** Module specifier relative to the feature file, e.g. `./server/queries/getTasks`. */ + from: string; +} + +/** + * A native Wasp spec declaration produced by a component generator, ready to be + * inserted into a feature's `spec` array by {@link ConfigGenerator.update}. + */ +export interface SpecDeclaration { + /** The spec constructor kind, e.g. `query`, `route`, `crud`, `job`. */ + kind: string; + /** + * The full constructor call text, e.g. + * `query(getTasks, { entities: ["Task"], auth: true })`. + */ + call: string; + /** The `with { type: "ref" }` imports the call references. */ + refImports: RefImport[]; +} + +/** + * Interface for Wasp configuration generators. * @interface ConfigGenerator */ export interface ConfigGenerator { /** - * Generate configuration from string argument - * @param args - String configuration argument + * Generate a feature configuration file in the given feature directory. + * @param args - The feature directory path */ generate(args: string): Promise | void; /** - * Updates a declaration in a Wasp configuration file + * Inserts or replaces a declaration in a feature's `spec` array (and keeps its + * imports in sync). * @param featurePath - The path to the feature directory - * @param declaration - The declaration to add or update - * @returns The updated feature configuration file + * @param declaration - The native spec declaration to add or update + * @returns The path to the updated feature configuration file */ - update(featurePath: string, declaration: string): string; + update(featurePath: string, declaration: SpecDeclaration): string; } diff --git a/packages/swarm-wasp/src/generators/config/index.ts b/packages/swarm-wasp/src/generators/config/index.ts index f1ac9d15..077a9736 100644 --- a/packages/swarm-wasp/src/generators/config/index.ts +++ b/packages/swarm-wasp/src/generators/config/index.ts @@ -1,2 +1,3 @@ -export type { ConfigGenerator } from './config-generator'; +export { FeaturesBarrelGenerator } from './barrel-generator'; +export type { ConfigGenerator, SpecDeclaration } from './config-generator'; export { WaspConfigGenerator } from './wasp-config-generator'; diff --git a/packages/swarm-wasp/src/generators/config/templates/api-namespace.eta b/packages/swarm-wasp/src/generators/config/templates/api-namespace.eta deleted file mode 100644 index a2b8835e..00000000 --- a/packages/swarm-wasp/src/generators/config/templates/api-namespace.eta +++ /dev/null @@ -1,3 +0,0 @@ - .addApiNamespace(feature, "<%=namespaceName%>", { - path: "<%=pathValue%>", - }) diff --git a/packages/swarm-wasp/src/generators/config/templates/api.eta b/packages/swarm-wasp/src/generators/config/templates/api.eta deleted file mode 100644 index d21cf494..00000000 --- a/packages/swarm-wasp/src/generators/config/templates/api.eta +++ /dev/null @@ -1,11 +0,0 @@ - .addApi(feature, "<%=apiName%>", { - method: "<%=method%>", - route: "<%=route%>", -<%- if (entities.length > 0) { %> - entities: [<%=entities%>], -<%- } %> - auth: <%=auth%>, -<%- if (customMiddleware) { %> - customMiddleware: true, -<%- } %> - }) diff --git a/packages/swarm-wasp/src/generators/config/templates/crud.eta b/packages/swarm-wasp/src/generators/config/templates/crud.eta deleted file mode 100644 index d85c21bb..00000000 --- a/packages/swarm-wasp/src/generators/config/templates/crud.eta +++ /dev/null @@ -1,4 +0,0 @@ - .addCrud(feature, "<%=crudName%>", { - entity: "<%=dataType%>", - <%=operations%>, - }) diff --git a/packages/swarm-wasp/src/generators/config/templates/job.eta b/packages/swarm-wasp/src/generators/config/templates/job.eta deleted file mode 100644 index 8173f832..00000000 --- a/packages/swarm-wasp/src/generators/config/templates/job.eta +++ /dev/null @@ -1,9 +0,0 @@ - .addJob(feature, "<%=jobName%>", { - cron: "<%=cron%>", -<%- if (args !== '{}') { %> - args: <%=args%>, -<%- } %> -<%- if (entities.length > 0) { %> - entities: [<%=entities%>], -<%- } %> - }) diff --git a/packages/swarm-wasp/src/generators/config/templates/operation.eta b/packages/swarm-wasp/src/generators/config/templates/operation.eta deleted file mode 100644 index 1c64301f..00000000 --- a/packages/swarm-wasp/src/generators/config/templates/operation.eta +++ /dev/null @@ -1,6 +0,0 @@ - .add<%=operationType%>(feature, "<%=operationName%>", { -<%- if (entities.length > 0) { %> - entities: [<%=entities%>], -<%- } %> - auth: <%=auth%>, - }) diff --git a/packages/swarm-wasp/src/generators/config/templates/route.eta b/packages/swarm-wasp/src/generators/config/templates/route.eta deleted file mode 100644 index ad6bd0e3..00000000 --- a/packages/swarm-wasp/src/generators/config/templates/route.eta +++ /dev/null @@ -1,4 +0,0 @@ - .addRoute(feature, "<%=routeName%>", { - path: "<%=routePath%>", - auth: <%=auth%>, - }) diff --git a/packages/swarm-wasp/src/generators/config/wasp-config-generator.test.ts b/packages/swarm-wasp/src/generators/config/wasp-config-generator.test.ts index c5943c21..ac5ff0ac 100644 --- a/packages/swarm-wasp/src/generators/config/wasp-config-generator.test.ts +++ b/packages/swarm-wasp/src/generators/config/wasp-config-generator.test.ts @@ -1,8 +1,20 @@ import type { FileSystem, Logger } from '@ingenyus/swarm'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createMockFS, createMockLogger } from '../../../tests/utils'; +import type { SpecDeclaration } from './config-generator'; import { WaspConfigGenerator } from './wasp-config-generator'; +const EMPTY_SPEC = `import { type Spec } from '@wasp.sh/spec'; + +export const spec: Spec = []; +`; + +const query: SpecDeclaration = { + kind: 'query', + call: 'query(getTask, { entities: ["Task"], auth: true })', + refImports: [{ names: ['getTask'], from: './server/queries/getTask' }], +}; + describe('WaspConfigGenerator', () => { let fs: FileSystem; let logger: Logger; @@ -12,180 +24,87 @@ describe('WaspConfigGenerator', () => { fs = createMockFS(); logger = createMockLogger(); gen = new WaspConfigGenerator(logger, fs); + // findWaspRoot resolves immediately; feature config exists by default. + fs.existsSync = vi.fn(() => true); }); - it('generate creates feature config', () => { - // Mock template path to exist, but config file to not exist - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.includes('feature.wasp.eta')) return true; // Template exists - if (typeof p === 'string' && p.endsWith('/feature.wasp.ts')) return false; // New config file doesn't exist - if (typeof p === 'string' && p.includes('test-feature.wasp.ts')) - return false; // Old-named config doesn't exist - return true; // Feature directory exists - }); - fs.readFileSync = vi.fn(() => 'template'); - fs.writeFileSync = vi.fn(); + it('generate copies the feature template when the config does not exist', () => { + fs.existsSync = vi.fn((p) => !String(p).endsWith('feature.wasp.ts')); fs.copyFileSync = vi.fn(); - gen.generate('test-feature'); + gen.generate('todo'); expect(fs.copyFileSync).toHaveBeenCalled(); }); - it('update updates existing config', () => { - const initialContent = `import { App } from "@ingenyus/swarm-wasp"; + it('update inserts a native declaration plus its imports into the spec array', () => { + fs.readFileSync = vi.fn(() => EMPTY_SPEC); + fs.writeFileSync = vi.fn(); -export default function configure(app: App, feature: string): void { - app -}`; + gen.update('todo', query); - fs.existsSync = vi.fn(() => true); - fs.readFileSync = vi.fn(() => initialContent); - fs.writeFileSync = vi.fn(); + const written = (fs.writeFileSync as ReturnType).mock + .calls[0][1] as string; - const result = gen.update( - 'test-feature', - '.addJob(feature, "testJob", { cron: "0 8 * * *", args: {} })' + expect(written).toContain( + 'query(getTask, { entities: ["Task"], auth: true })' + ); + expect(written).toContain( + `import { getTask } from './server/queries/getTask' with { type: 'ref' };` + ); + expect(written).toContain('// Query definitions'); + expect(written).toMatch( + /import \{ type Spec, query \} from '@wasp\.sh\/spec'/ ); - - expect(fs.writeFileSync).toHaveBeenCalled(); - expect(typeof result).toBe('string'); }); - it('update creates config file if it does not exist', () => { - // Mock template file to exist, but config file to not exist - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.includes('feature.wasp.eta')) return true; // Template exists - if (typeof p === 'string' && p.endsWith('/feature.wasp.ts')) return false; // New config file doesn't exist - if (typeof p === 'string' && p.includes('test-feature.wasp.ts')) - return false; // Old-named config doesn't exist - return true; // Other files exist - }); - fs.copyFileSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'template content'; - }); + it('update is idempotent for the same declaration', () => { + fs.readFileSync = vi.fn(() => EMPTY_SPEC); fs.writeFileSync = vi.fn(); + gen.update('todo', query); + const once = (fs.writeFileSync as ReturnType).mock + .calls[0][1] as string; - const result = gen.update( - 'test-feature', - '.addJob(feature, "testJob", { cron: "0 8 * * *", args: {} })' - ); + fs.readFileSync = vi.fn(() => once); + fs.writeFileSync = vi.fn(); + gen.update('todo', query); + const twice = (fs.writeFileSync as ReturnType).mock + .calls[0][1] as string; - expect(fs.copyFileSync).toHaveBeenCalled(); - expect(fs.writeFileSync).toHaveBeenCalled(); - expect(typeof result).toBe('string'); + expect(twice).toBe(once); }); - describe('semicolon handling', () => { - it('handles existing semicolons in config', () => { - const contentWithSemicolons = `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app - // Action definitions - .addAction(feature, "createTask", { - entities: ["Task"], - auth: false, - }); - // Query definitions - .addQuery(feature, "getTask", { - entities: ["Task"], - auth: false, - }); -}`; - - fs.existsSync = vi.fn(() => true); - fs.readFileSync = vi.fn(() => contentWithSemicolons); - fs.writeFileSync = vi.fn(); - - const result = gen.update( - 'test-feature', - '.addQuery(feature, "getAllTasks", { entities: ["Task"], auth: false })' - ); - - expect(fs.writeFileSync).toHaveBeenCalled(); - - const writtenContent = (fs.writeFileSync as any).mock.calls[0][1]; - const lines = writtenContent.split('\n'); - const methodChainLines = lines.filter( - (l: string) => l.trim().endsWith(')') || l.trim().endsWith(');') - ); - - methodChainLines.forEach((line: string, index: number) => { - if (index === methodChainLines.length - 1) { - expect(line.trim()).toMatch(/\);$/); - } else { - expect(line.trim()).toMatch(/\)$/); - expect(line.trim()).not.toMatch(/\);$/); - } - }); - }); - - it('preserves semicolons outside method chain', () => { - const contentWithSemicolons = `import { App } from "@ingenyus/swarm-wasp"; - -const someVar = "test"; - -export default function configureFeature(app: App, feature: string): void { - app - .addAction(feature, "createTask", { - entities: ["Task"], - auth: false, - }); -}`; - - fs.existsSync = vi.fn(() => true); - fs.readFileSync = vi.fn(() => contentWithSemicolons); - fs.writeFileSync = vi.fn(); - - gen.update( - 'test-feature', - '.addQuery(feature, "getTask", { entities: ["Task"], auth: false })' - ); - - const writtenContent = (fs.writeFileSync as any).mock.calls[0][1]; - - expect(writtenContent).toContain('const someVar = "test";'); - }); - - it('handles mixed semicolon usage', () => { - const mixedContent = `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app - .addAction(feature, "createTask", { - entities: ["Task"], - auth: false, - }); - .addAction(feature, "updateTask", { - entities: ["Task"], - auth: false, - }) - .addQuery(feature, "getTask", { - entities: ["Task"], - auth: false, - }); -}`; - - fs.existsSync = vi.fn(() => true); - fs.readFileSync = vi.fn(() => mixedContent); - fs.writeFileSync = vi.fn(); - - const result = gen.update( - 'test-feature', - '.addQuery(feature, "getAllTasks", { entities: ["Task"], auth: false })' - ); - - expect(fs.writeFileSync).toHaveBeenCalled(); - expect(typeof result).toBe('string'); - }); + it('has detects an existing declaration by kind and identity', () => { + fs.readFileSync = vi.fn( + () => `import { type Spec, query } from '@wasp.sh/spec'; +import { getTask } from './server/queries/getTask' with { type: 'ref' }; + +export const spec: Spec = [ + query(getTask, { auth: true }), +]; +` + ); + + expect( + gen.has('todo', { + kind: 'query', + call: 'query(getTask, {})', + refImports: [], + }) + ).toBe(true); + expect( + gen.has('todo', { + kind: 'query', + call: 'query(getOther, {})', + refImports: [], + }) + ).toBe(false); + expect( + gen.has('todo', { + kind: 'action', + call: 'action(getTask, {})', + refImports: [], + }) + ).toBe(false); }); }); diff --git a/packages/swarm-wasp/src/generators/config/wasp-config-generator.ts b/packages/swarm-wasp/src/generators/config/wasp-config-generator.ts index 74d3ad0d..3d4951d6 100644 --- a/packages/swarm-wasp/src/generators/config/wasp-config-generator.ts +++ b/packages/swarm-wasp/src/generators/config/wasp-config-generator.ts @@ -1,13 +1,38 @@ +import path from 'node:path'; import { - FileSystem, + type FileSystem, getCLILogger, handleFatalError, - Logger, - parseHelperMethodDefinition, + type Logger, } from '@ingenyus/swarm'; -import path from 'node:path'; +import ts from 'typescript'; import { getFeatureDir, realFileSystem, TemplateUtility } from '../../common'; -import { ConfigGenerator } from '../../generators/config'; +import type { ConfigGenerator, SpecDeclaration } from '.'; + +const SPEC_PKG = '@wasp.sh/spec'; +const INDENT = ' '; + +/** Group-comment header rendered above the first element of each kind. */ +const KIND_COMMENT: Record = { + route: '// Route definitions', + api: '// Api definitions', + apiNamespace: '// ApiNamespace definitions', + crud: '// Crud definitions', + action: '// Action definitions', + query: '// Query definitions', + job: '// Job definitions', +}; + +interface SpecElement { + kind: string; + identity: string; + text: string; +} + +interface ParsedRefImport { + names: Set; + from: string; +} export class WaspConfigGenerator implements ConfigGenerator { protected path = path; @@ -22,9 +47,6 @@ export class WaspConfigGenerator implements ConfigGenerator { /** * Gets the template path for feature config templates. - * Feature config templates are located in the feature generator's templates directory. - * @param templateName - The name of the template file (e.g., 'feature.wasp.eta') - * @returns The full path to the template file */ private getTemplatePath(templateName: string): string { return this.templateUtility.resolveTemplatePath( @@ -35,17 +57,15 @@ export class WaspConfigGenerator implements ConfigGenerator { } /** - * Generate a TypeScript Wasp config file in a feature directory + * Generate a TypeScript Wasp config file in a feature directory. * @param featurePath - The feature directory path */ generate(featurePath: string): void { - // Create feature directory if it doesn't exist const featureDir = getFeatureDir(this.fileSystem, featurePath); if (!this.fileSystem.existsSync(featureDir)) { this.fileSystem.mkdirSync(featureDir, { recursive: true }); } - // Generate feature config in the feature directory const templatePath = this.getTemplatePath('feature.wasp.eta'); if (!this.fileSystem.existsSync(templatePath)) { @@ -65,12 +85,13 @@ export class WaspConfigGenerator implements ConfigGenerator { } /** - * Updates or creates a feature configuration file with a pre-built declaration. + * Inserts or replaces a declaration in a feature's `spec` array, keeping the + * `@wasp.sh/spec` named imports and the `with { type: "ref" }` imports in sync. * @param featurePath - The path to the feature - * @param declaration - The pre-built declaration string to add or update - * @returns The updated feature configuration file + * @param declaration - The native spec declaration to add or update + * @returns The path to the updated feature configuration file */ - update(featurePath: string, declaration: string): string { + update(featurePath: string, declaration: SpecDeclaration): string { const configDir = getFeatureDir(this.fileSystem, featurePath); const configFilePath = path.join(configDir, `feature.wasp.ts`); @@ -84,532 +105,292 @@ export class WaspConfigGenerator implements ConfigGenerator { this.fileSystem.copyFileSync(templatePath, configFilePath); } - let content = this.fileSystem.readFileSync(configFilePath, 'utf8'); - - content = this.normaliseSemicolons(content); - - const parsed = parseHelperMethodDefinition(declaration); - - if (!parsed) { - handleFatalError(`Could not parse definition: ${declaration}`); - return content; - } - - const { methodName } = parsed; - - // Check if there are any existing definitions of this type before removal - const hadExistingDefinitions = this.hasExistingDefinitions( - content, - methodName - ); + const content = this.fileSystem.readFileSync(configFilePath, 'utf8'); + const updated = this.applyDeclaration(content, declaration); - // Remove existing definition before adding new one - content = this.removeExistingDefinition(content, declaration); + this.fileSystem.writeFileSync(configFilePath, updated); - // Check if there are any existing definitions of this type after removal - const hasExistingDefinitions = this.hasExistingDefinitions( - content, - methodName - ); + return configFilePath; + } - // Find the position to insert the new definition - const lines = content.split('\n'); - const configureFunctionStart = lines.findIndex((line) => - line.trim().startsWith('export default function') - ); + /** + * Whether the feature's `spec` array already contains an element with the same + * kind and identity as `declaration`. + * @param featurePath - The path to the feature + * @param declaration - The declaration to check for + */ + has(featurePath: string, declaration: SpecDeclaration): boolean { + const configDir = getFeatureDir(this.fileSystem, featurePath); + const configFilePath = path.join(configDir, `feature.wasp.ts`); - if (configureFunctionStart === -1) { - handleFatalError('Could not find configure function in feature config'); + if (!this.fileSystem.existsSync(configFilePath)) { + return false; } - // Find the app line inside the configure function - const appLineIndex = lines.findIndex( - (line, index) => index > configureFunctionStart && line.trim() === 'app' - ); - - if (appLineIndex === -1) { - // No existing app chain, add it after the opening brace - const insertIndex = configureFunctionStart + 1; - const itemsToInsert = [' app']; - - // Always add comment for the first entry since it's the first item of its type - const comment = this.getMethodComment(methodName); - itemsToInsert.push(` ${comment}`); - - itemsToInsert.push(declaration.trimEnd()); - lines.splice(insertIndex, 0, ...itemsToInsert); - } else { - // Find the correct insertion point based on ordering - // Use hadExistingDefinitions to determine if we should add a comment - // If there were existing definitions before removal, don't add a comment (we're replacing) - const { insertIndex, addComment } = this.findGroupInsertionPoint( - lines, - methodName, - declaration, - hadExistingDefinitions || hasExistingDefinitions - ); + const content = this.fileSystem.readFileSync(configFilePath, 'utf8'); + const arr = this.findSpecArray(this.parse(content)); - // Insert with proper spacing - const newLines = this.insertWithSpacing( - lines, - declaration, - insertIndex, - methodName, - addComment - ); - const normalisedContent = this.normaliseSemicolons(newLines.join('\n')); - this.fileSystem.writeFileSync(configFilePath, normalisedContent); - return configFilePath; + if (!arr) { + return false; } - const normalisedContent = this.normaliseSemicolons(lines.join('\n')); - this.fileSystem.writeFileSync(configFilePath, normalisedContent); - - return configFilePath; - } + const identity = this.identityOf(declaration.kind, declaration.call); - /** - * Determines the insertion index for a method name based on alphabetical ordering - * of existing groups in the configuration file. - * @param groups - Object containing existing method groups - * @param methodName - The method name to find insertion index for - * @returns The insertion index for the method name - */ - private getInsertionIndexForMethod( - groups: Record, - methodName: string - ): number { - const existingMethods = Object.keys(groups).filter( - (method) => groups[method].length > 0 + return this.extractElements(this.parse(content), arr).some( + (element) => + element.kind === declaration.kind && element.identity === identity ); - const allMethods = [...existingMethods, methodName].sort(); - - return allMethods.indexOf(methodName); } - /** - * Gets the comment text for a method type. - * @param methodName The method name (e.g., 'addApi') - * @returns The comment text for the method type - */ - private getMethodComment(methodName: string): string { - const entityName = methodName.startsWith('add') - ? methodName.slice(3) - : methodName; - - return `// ${entityName} definitions`; - } + /* ---------------------------------------------------------------------- */ + /* Spec-file editing (AST-located, text-spliced) */ + /* ---------------------------------------------------------------------- */ /** - * Finds the correct insertion point for a new configuration item. - * @param lines - Array of file lines - * @param methodName - The method name (e.g., 'addApi') - * @param definition - The definition string to parse for item name - * @returns Object with insertion index and whether to add a comment + * Returns `content` with `declaration` inserted into (or replacing a + * same-identity element within) the `export const spec` array, coordinating + * the three edit sites: the array element, the `with { type: "ref" }` + * imports, and the `@wasp.sh/spec` named-import list. Idempotent. */ - private findGroupInsertionPoint( - lines: string[], - methodName: string, - definition: string, - hasExistingDefinitionsOfType: boolean - ): { insertIndex: number; addComment: boolean } { - const appLineIndex = lines.findIndex((line) => line.trim() === 'app'); - - if (appLineIndex === -1) { - return { insertIndex: appLineIndex + 1, addComment: false }; // Insert after app line - } - - // Find all existing method calls and their positions - const methodCalls: Array<{ - lineIndex: number; - endLineIndex: number; - methodName: string; - itemName: string; - }> = []; - - for (let i = appLineIndex + 1; i < lines.length; i++) { - const line = lines[i].trim(); - - if (line.startsWith('.') && line.includes('(')) { - // Look for the method call across multiple lines - let methodCallContent = line; - let j = i; - let closingParenCount = 0; - let foundClosingParen = false; - - // Count opening and closing parentheses to find the complete method call - for (let k = 0; k < methodCallContent.length; k++) { - if (methodCallContent[k] === '(') closingParenCount++; - if (methodCallContent[k] === ')') closingParenCount--; - if (closingParenCount === 0 && methodCallContent[k] === ')') { - foundClosingParen = true; - break; - } - } - - // If not found on this line, look at subsequent lines - while (!foundClosingParen && j < lines.length - 1) { - j++; - methodCallContent += ' ' + lines[j].trim(); - for (let k = 0; k < lines[j].length; k++) { - if (lines[j][k] === '(') closingParenCount++; - if (lines[j][k] === ')') closingParenCount--; - if (closingParenCount === 0 && lines[j][k] === ')') { - foundClosingParen = true; - break; - } - } - } + private applyDeclaration(content: string, decl: SpecDeclaration): string { + const sf = this.parse(content); + const arr = this.findSpecArray(sf); - const match = methodCallContent.match( - /\.(\w+)\([^,]+,\s*['"`]([^'"`]+)['"`]/ - ); - if (match) { - methodCalls.push({ - lineIndex: i, - endLineIndex: j, - methodName: match[1], - itemName: match[2], - }); - } - } + if (!arr) { + handleFatalError( + 'Could not find an `export const spec` array in the feature config' + ); + return content; } - // Group method calls by type - const groups: Record< - string, - Array<{ lineIndex: number; endLineIndex: number; itemName: string }> - > = {}; - methodCalls.forEach((call) => { - if (!groups[call.methodName]) { - groups[call.methodName] = []; + // 1. Element list: drop same-identity, add the new declaration. + const identity = this.identityOf(decl.kind, decl.call); + const elements = this.extractElements(sf, arr).filter( + (element) => + !(element.kind === decl.kind && element.identity === identity) + ); + elements.push({ kind: decl.kind, identity, text: decl.call.trim() }); + const newArrayText = this.renderArray(elements); + + // 2. `@wasp.sh/spec` import rebuilt from the kinds actually used. + const newSpecImport = this.renderSpecImport(newArrayText); + + // 3. Ref imports: merge the declaration's, evict re-homed identifiers, prune. + const refs = this.extractRefImports(sf); + for (const ri of decl.refImports ?? []) { + let existing = refs.find((ref) => ref.from === ri.from); + if (!existing) { + existing = { names: new Set(), from: ri.from }; + refs.push(existing); } - groups[call.methodName].push({ - lineIndex: call.lineIndex, - endLineIndex: call.endLineIndex, - itemName: call.itemName, - }); - }); - - // Find the target group - const targetGroup = groups[methodName] || []; - - if (targetGroup.length === 0) { - // No items of this type exist, find position based on alphabetical ordering - const targetGroupIndex = this.getInsertionIndexForMethod( - groups, - methodName - ); - const existingMethods = Object.keys(groups) - .filter((method) => groups[method].length > 0) - .sort(); - - // Look for the first group that comes after the target group alphabetically - for (let i = targetGroupIndex; i < existingMethods.length; i++) { - const groupMethod = existingMethods[i]; - if (groups[groupMethod] && groups[groupMethod].length > 0) { - // Insert before the first item of the next group - const firstItem = groups[groupMethod][0]; - let insertIndex = firstItem.lineIndex; - - // Check if there's a comment line before the first item - for (let j = firstItem.lineIndex - 1; j > appLineIndex; j--) { - const line = lines[j].trim(); - if (line.startsWith('//') && line.includes('definitions')) { - insertIndex = j; // Insert before the comment - break; - } else if (line.startsWith('.') || line === '') { - // Skip empty lines and method calls - continue; - } else { - // Stop at non-comment, non-empty, non-method lines - break; - } - } - - // Only add comment if this is the first item of its type - return { insertIndex, addComment: !hasExistingDefinitionsOfType }; + for (const name of ri.names) { + // An identifier can only be imported once — evict it from other paths. + for (const other of refs) { + if (other !== existing) other.names.delete(name); } + existing.names.add(name); } - - // If no later groups exist, look for the last item of the previous group - for (let i = targetGroupIndex - 1; i >= 0; i--) { - const groupMethod = existingMethods[i]; - - if (groups[groupMethod] && groups[groupMethod].length > 0) { - const lastItem = groups[groupMethod][groups[groupMethod].length - 1]; - - return { - insertIndex: lastItem.endLineIndex + 1, - addComment: !hasExistingDefinitionsOfType, - }; + } + for (const ref of refs) { + for (const name of [...ref.names]) { + if (!new RegExp(`\\b${name}\\b`).test(newArrayText)) { + ref.names.delete(name); } } - - return { - insertIndex: appLineIndex + 1, - addComment: !hasExistingDefinitionsOfType, - }; // Insert after app line if no groups exist } + const newRefImports = this.renderRefImports(refs); - // Parse the new item name - const parsed = parseHelperMethodDefinition(definition); + // Apply edits bottom-to-top so offsets stay valid. + const edits: Array<{ start: number; end: number; text: string }> = []; - if (!parsed) { - return { insertIndex: appLineIndex + 1, addComment: false }; // Fallback if parsing fails + const specImportNode = sf.statements.find( + (st): st is ts.ImportDeclaration => + ts.isImportDeclaration(st) && + ts.isStringLiteral(st.moduleSpecifier) && + st.moduleSpecifier.text === SPEC_PKG + ); + if (specImportNode) { + edits.push({ + start: specImportNode.getStart(sf), + end: specImportNode.getEnd(), + text: `${newSpecImport}\n${newRefImports}`, + }); } - const { firstParam: itemName } = parsed; - - // Find the correct insertion point by comparing with existing items - // We need to find where this item should be inserted alphabetically - for (let i = 0; i < targetGroup.length; i++) { - if (itemName.localeCompare(targetGroup[i].itemName) < 0) { - // Insert before this item - // If we're inserting at the beginning of the group, we need to add the comment - return { insertIndex: targetGroup[i].lineIndex, addComment: false }; - } + for (const st of sf.statements) { + if (!ts.isImportDeclaration(st) || !this.isRefImport(st, sf)) continue; + let end = st.getEnd(); + if (content[end] === '\n') end += 1; + edits.push({ start: st.getStart(sf), end, text: '' }); } - // If we get here, the new item should be inserted after the last item - const lastItem = targetGroup[targetGroup.length - 1]; + edits.push({ + start: arr.getStart(sf), + end: arr.getEnd(), + text: newArrayText, + }); - return { insertIndex: lastItem.endLineIndex + 1, addComment: false }; - } + edits.sort((a, b) => b.start - a.start); - /** - * Inserts a definition with optional comment header. - * @param lines - Array of file lines - * @param declaration - The declaration to insert - * @param insertIndex - The index where to insert - * @param methodName - The method name for comment generation - * @param addComment - Whether to add a comment before the declaration - * @returns The modified lines array - */ - private insertWithSpacing( - lines: string[], - declaration: string, - insertIndex: number, - methodName: string, - addComment: boolean = false - ): string[] { - const newLines = [...lines]; - - // Add comment if this is the first item of its type - if (addComment) { - const comment = this.getMethodComment(methodName); - - newLines.splice(insertIndex, 0, ` ${comment}`); - insertIndex += 1; // Adjust for the added comment line + let out = content; + for (const edit of edits) { + out = out.slice(0, edit.start) + edit.text + out.slice(edit.end); } - // Insert the declaration (trim any trailing newlines) - newLines.splice(insertIndex, 0, declaration.trimEnd()); - - return newLines; + return out.replace(/\n{3,}/g, '\n\n'); } - /** - * Checks if there are any existing definitions of a specific type in the content. - * @param content - The file content to search - * @param methodName - The method name to check for (e.g., 'addJob', 'addApi') - * @returns true if there are existing definitions of this type, false otherwise - */ - private hasExistingDefinitions(content: string, methodName: string): boolean { - const lines = content.split('\n'); - - for (const line of lines) { - if (line.trim().startsWith(`.${methodName}(`)) { - return true; - } - } - - return false; + private parse(content: string): ts.SourceFile { + return ts.createSourceFile( + 'feature.wasp.ts', + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS + ); } /** - * Removes an existing definition from the content by finding the helper method call - * and removing the entire method call block. - * @param content - The file content - * @param definition - The new definition to find the existing one from - * @returns The content with the existing definition removed + * The stable identity of a spec element, keyed by constructor kind. Used to + * detect the same declaration for replacement and to sort within a group. */ - private removeExistingDefinition( - content: string, - definition: string - ): string { - const parsed = parseHelperMethodDefinition(definition); - - if (!parsed) { - return content; + private identityOf(kind: string, call: string): string { + const sf = this.parse(`const _ = ${call};`); + const decl = sf.statements[0]; + if (!ts.isVariableStatement(decl)) return call; + const init = decl.declarationList.declarations[0]?.initializer; + if (!init || !ts.isCallExpression(init)) return call; + + const args = init.arguments; + const str = (n: ts.Node | undefined) => + n && ts.isStringLiteral(n) ? n.text : undefined; + const id = (n: ts.Node | undefined) => + n && ts.isIdentifier(n) ? n.text : undefined; + + switch (kind) { + case 'route': + case 'crud': + case 'apiNamespace': + return str(args[0]) ?? call; + case 'api': + return id(args[2]) ?? call; + default: + return id(args[0]) ?? str(args[0]) ?? call; } + } - const { methodName, firstParam } = parsed; - let contentLines = content.split('\n'); - - // Find and remove any existing definition - let openingLineIndex = -1; - - for (let i = 0; i < contentLines.length; i++) { - const line = contentLines[i]; - - if (line.trim().startsWith(`.${methodName}(`)) { - // Check if this is the right method call by comparing the first parameter - if (firstParam && line.includes(firstParam)) { - openingLineIndex = i; - break; + private findSpecArray(sf: ts.SourceFile): ts.ArrayLiteralExpression | null { + for (const st of sf.statements) { + if (!ts.isVariableStatement(st)) continue; + for (const decl of st.declarationList.declarations) { + if ( + ts.isIdentifier(decl.name) && + decl.name.text === 'spec' && + decl.initializer && + ts.isArrayLiteralExpression(decl.initializer) + ) { + return decl.initializer; } } } + return null; + } - if (openingLineIndex === -1) { - return content; - } - - // Find the closing parenthesis - let closingLineIndex = -1; - let parenCount = 0; - let braceCount = 0; - let foundOpening = false; - - for (let i = openingLineIndex; i < contentLines.length; i++) { - const line = contentLines[i]; - - for (const char of line) { - if (char === '(') { - parenCount++; - foundOpening = true; - } else if (char === ')') { - parenCount--; - if (foundOpening && parenCount === 0 && braceCount === 0) { - // Found the closing parenthesis for the method call - closingLineIndex = i; - break; - } - } else if (char === '{') { - braceCount++; - } else if (char === '}') { - braceCount--; - } - } + private extractElements( + sf: ts.SourceFile, + arr: ts.ArrayLiteralExpression + ): SpecElement[] { + return arr.elements.map((element) => { + const kind = + ts.isCallExpression(element) && ts.isIdentifier(element.expression) + ? element.expression.text + : '?'; - if (closingLineIndex !== -1) { - break; - } - } + return { + kind, + identity: this.identityOf(kind, element.getText(sf)), + text: element.getText(sf), + }; + }); + } - if (closingLineIndex === -1) { - this.logger.warn( - 'Could not find closing parenthesis for existing definition' - ); - return content; - } + private renderArray(elements: SpecElement[]): string { + if (elements.length === 0) return '[]'; - // Remove the lines containing the existing definition - contentLines.splice( - openingLineIndex, - closingLineIndex - openingLineIndex + 1 + const sorted = [...elements].sort( + (a, b) => + a.kind.localeCompare(b.kind) || + String(a.identity).localeCompare(String(b.identity)) ); - return contentLines.join('\n'); - } + const lines: string[] = []; + let lastKind: string | null = null; - /** - * Adds a definition to the content by finding the appropriate place to insert it. - * @param content - The current file content - * @param definition - The definition to add - * @returns The updated content with the new definition - */ - private addDefinitionToContent(content: string, definition: string): string { - const lines = content.split('\n'); - const lastLineIndex = lines.length - 1; - - // Find the last line that's not empty and not a closing brace - let insertIndex = lastLineIndex; - for (let i = lastLineIndex; i >= 0; i--) { - const line = lines[i].trim(); - if (line && !line.startsWith('}')) { - insertIndex = i; - break; + for (const element of sorted) { + if (element.kind !== lastKind) { + lines.push( + `${INDENT}${KIND_COMMENT[element.kind] ?? `// ${element.kind}`}` + ); + lastKind = element.kind; } + lines.push(`${INDENT}${element.text},`); } - // Insert the definition before the last closing brace - lines.splice(insertIndex + 1, 0, ` ${definition}`); - - return lines.join('\n'); + return `[\n${lines.join('\n')}\n]`; } - /** - * Normalises semicolons in the config file by removing them from method chain calls - * while preserving them in other contexts (imports, declarations, etc.). - * @param content - The file content to normalise - * @returns The normalised content - */ - private normaliseSemicolons(content: string): string { - const lines = content.split('\n'); - const configureFunctionStart = lines.findIndex((line) => - line.trim().startsWith('export default function') - ); - - if (configureFunctionStart === -1) { - return content; - } - - const appLineIndex = lines.findIndex( - (line, index) => - index > configureFunctionStart && line.trim().startsWith('app') - ); - - if (appLineIndex === -1) { - return content; + /** Rebuilds the `@wasp.sh/spec` import from the constructors used in the array. */ + private renderSpecImport(arrayText: string): string { + const kinds = new Set(); + for (const kind of Object.keys(KIND_COMMENT)) { + if (new RegExp(`\\b${kind}\\s*\\(`).test(arrayText)) kinds.add(kind); } + if (/\bpage\s*\(/.test(arrayText)) kinds.add('page'); - let braceCount = 0; - let functionEndIndex = lines.length - 1; + const names = ['type Spec', ...[...kinds].sort()]; - for (let i = configureFunctionStart; i < lines.length; i++) { - const line = lines[i]; + return `import { ${names.join(', ')} } from '${SPEC_PKG}';`; + } - for (const char of line) { - if (char === '{') braceCount++; - if (char === '}') { - braceCount--; + private isRefImport(node: ts.ImportDeclaration, sf: ts.SourceFile): boolean { + const attributes = node.attributes; + return Boolean( + attributes?.elements.some( + (attr) => + attr.name.getText(sf) === 'type' && + ts.isStringLiteral(attr.value) && + attr.value.text === 'ref' + ) + ); + } - if (braceCount === 0) { - functionEndIndex = i; + private extractRefImports(sf: ts.SourceFile): ParsedRefImport[] { + const refs: ParsedRefImport[] = []; - break; - } - } - } + for (const st of sf.statements) { + if (!ts.isImportDeclaration(st) || !this.isRefImport(st, sf)) continue; + if (!ts.isStringLiteral(st.moduleSpecifier)) continue; - if (braceCount === 0 && i > configureFunctionStart) { - break; - } - } + const bindings = st.importClause?.namedBindings; + const names = + bindings && ts.isNamedImports(bindings) + ? bindings.elements.map((element) => element.name.text) + : []; - let lastMethodCallIndex = -1; - for (let i = appLineIndex + 1; i < functionEndIndex; i++) { - const line = lines[i]; - const trimmed = line.trim(); - - if ( - (trimmed.endsWith(')') || trimmed.endsWith(');')) && - !trimmed.startsWith('//') - ) { - lines[i] = line.replace(/;\s*$/, ''); - lastMethodCallIndex = i; - } + refs.push({ names: new Set(names), from: st.moduleSpecifier.text }); } - if ( - lastMethodCallIndex !== -1 && - !lines[lastMethodCallIndex].trim().endsWith(';') - ) { - lines[lastMethodCallIndex] = lines[lastMethodCallIndex] + ';'; - } + return refs; + } - return lines.join('\n'); + private renderRefImports(refs: ParsedRefImport[]): string { + return refs + .filter((ref) => ref.names.size > 0) + .sort((a, b) => a.from.localeCompare(b.from)) + .map( + (ref) => + `import { ${[...ref.names].sort().join(', ')} } from '${ref.from}' with { type: 'ref' };` + ) + .join('\n'); } } diff --git a/packages/swarm-wasp/src/generators/crud/crud-generator.test.ts b/packages/swarm-wasp/src/generators/crud/crud-generator.test.ts index 412245e6..ec283d19 100644 --- a/packages/swarm-wasp/src/generators/crud/crud-generator.test.ts +++ b/packages/swarm-wasp/src/generators/crud/crud-generator.test.ts @@ -1,77 +1,44 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { CrudGenerator } from './crud-generator'; import { schema } from './schema'; describe('CrudGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: CrudGenerator; beforeEach(async () => { - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(CrudGenerator, schema, { - fileSystem: fs, - }); + gen = await createTestGenerator(CrudGenerator, schema); }); - it('buildOperations should handle public, override, and exclude flags correctly', () => { - const operations = (gen as any).buildOperations({ + it('getDefinition builds a native crud declaration with public, override, and excluded ops', () => { + const decl = gen.getDefinition('tasks', 'Task', { + dataType: 'Task', + feature: 'todo', public: ['get', 'getAll'], override: ['create'], exclude: ['delete'], }); - expect(operations).toHaveProperty('get'); - expect(operations).toHaveProperty('getAll'); - expect(operations).toHaveProperty('create'); - expect(operations).toHaveProperty('update', {}); - expect(operations).not.toHaveProperty('delete'); - expect(operations.get).toEqual({ isPublic: true }); - expect(operations.getAll).toEqual({ isPublic: true }); - expect(operations.create).toHaveProperty('override', true); - }); - - it('buildOperations should handle default flags correctly', () => { - const operations = (gen as any).buildOperations({}); - - expect(operations).toHaveProperty('get', {}); - expect(operations).toHaveProperty('getAll', {}); - expect(operations).toHaveProperty('create', {}); - expect(operations).toHaveProperty('update', {}); - expect(operations).toHaveProperty('delete', {}); + expect(decl.kind).toBe('crud'); + // The crud is declared with a PascalCase name matching the generated type. + expect(decl.call).toBe( + 'crud("Tasks", "Task", { get: { isPublic: true }, getAll: { isPublic: true }, create: { overrideFn: createTask }, update: {} })' + ); + // Override fn is imported from the single generated crud file. + expect(decl.refImports).toEqual([ + { names: ['createTask'], from: './server/cruds/tasks' }, + ]); }); - it('getDefinition returns processed template', async () => { - // Mock the template utility - (gen as any).templateUtility = { - processTemplate: vi.fn( - () => 'app.addCrud("testCrud", { operations: {...} });' - ), - resolveTemplatePath: vi.fn( - (templateName) => `/mock/templates/${templateName}` - ), - }; + it('enables all operations by default with no ref imports', () => { + const decl = gen.getDefinition('tasks', 'Task', { + dataType: 'Task', + feature: 'todo', + }); - // Mock the getTemplatePath method to return a resolved promise - (gen as any).getTemplatePath = vi.fn(() => - Promise.resolve('/mock/templates/config/crud.eta') + expect(decl.call).toBe( + 'crud("Tasks", "Task", { get: {}, getAll: {}, create: {}, update: {}, delete: {} })' ); - - const result = await gen.getDefinition('testCrud', 'User', { - get: {}, - getAll: {}, - create: {}, - update: {}, - delete: {}, - }); - expect(typeof result).toBe('string'); + expect(decl.refImports).toEqual([]); }); }); diff --git a/packages/swarm-wasp/src/generators/crud/crud-generator.ts b/packages/swarm-wasp/src/generators/crud/crud-generator.ts index b40fe09c..ee75fafb 100644 --- a/packages/swarm-wasp/src/generators/crud/crud-generator.ts +++ b/packages/swarm-wasp/src/generators/crud/crud-generator.ts @@ -1,18 +1,18 @@ import { - GeneratorServices, getPlural, - Out, + type Out, toCamelCase, toPascalCase, } from '@ingenyus/swarm'; import { CONFIG_TYPES, - CrudOperation, - EntityMetadata, + type CrudOperation, + type EntityMetadata, getEntityMetadata, needsPrismaImport, } from '../../common'; import { OperationGeneratorBase } from '../base'; +import type { SpecDeclaration } from '../config'; import { schema } from './schema'; const CRUD_OPERATIONS_LIST: readonly CrudOperation[] = [ @@ -34,10 +34,6 @@ export class CrudGenerator extends OperationGeneratorBase< description = 'Generates a Wasp CRUD operation'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const { dataType, feature, name } = args; const crudName = name || toCamelCase(getPlural(dataType)); @@ -134,57 +130,23 @@ export class CrudGenerator extends OperationGeneratorBase< return imports.join('\n'); } - private async updateConfigFile( + private updateConfigFile( feature: string, crudName: string, dataType: string, args: Out, configPath: string ) { - const operations = this.buildOperations(args); - const definition = this.getDefinition(crudName, dataType, operations); + const definition = this.getDefinition(crudName, dataType, args); this.updateConfigWithCheck( configPath, - 'addCrud', - crudName, definition, feature, args.force || false ); } - private buildOperations(args: Out): Record { - const { - public: publicOps = [], - override: overrideOps = [], - exclude: excludeOps = [], - } = args; - - return CRUD_OPERATIONS_LIST.reduce( - (acc, operation) => { - if (excludeOps.includes(operation)) { - return acc; - } - - const operationConfig: Record = {}; - - if (publicOps.includes(operation)) { - operationConfig.isPublic = true; - } - - if (overrideOps.includes(operation)) { - operationConfig.override = true; - } - - acc[operation] = operationConfig; - - return acc; - }, - {} as Record - ); - } - /** * Generates operation code for overridden CRUD operations and returns as a single string. */ @@ -217,26 +179,61 @@ export class CrudGenerator extends OperationGeneratorBase< } /** - * Generates a CRUD definition for the feature configuration. + * Builds a native crud spec declaration for the feature configuration. + * Enabled operations render as `{}`, public ones add `isPublic: true`, and + * overridden ones add `overrideFn: ` (all overrides import from the + * single generated crud file). */ - getDefinition(crudName: string, dataType: string, operations: any): string { - const templatePath = this.templateUtility.resolveTemplatePath( - 'config/crud.eta', - 'crud', - import.meta.url - ); - const operationsStr = JSON.stringify(operations, null, 2) - .replace(/"([^"]+)":/g, '$1:') - .slice(1, -1) // Remove outer braces - .split('\n') - .filter((line) => line.trim() !== '') - .map((line, index) => (index === 0 ? line.trimStart() : ' ' + line)) - .join('\n'); - - return this.templateUtility.processTemplate(templatePath, { - crudName, - dataType, - operations: operationsStr, - }); + getDefinition( + crudName: string, + dataType: string, + args: Out + ): SpecDeclaration { + const { + public: publicOps = [], + override: overrideOps = [], + exclude: excludeOps = [], + } = args; + + const overrideNames: string[] = []; + const operationParts: string[] = []; + + for (const operation of CRUD_OPERATIONS_LIST) { + if (excludeOps.includes(operation)) { + continue; + } + + const inner: string[] = []; + + if (publicOps.includes(operation)) { + inner.push('isPublic: true'); + } + + if (overrideOps.includes(operation)) { + const fn = this.getOperationName(operation, dataType); + inner.push(`overrideFn: ${fn}`); + overrideNames.push(fn); + } + + operationParts.push( + `${operation}: ${inner.length ? `{ ${inner.join(', ')} }` : '{}'}` + ); + } + + // The crud declaration name must match the generated crud type namespace + // (`wasp/server/crud`) that override operations are typed against, so it is + // PascalCased; the override file itself is imported by its (camelCase) name. + const crudType = toPascalCase(crudName); + const call = `crud("${crudType}", "${dataType}", { ${operationParts.join(', ')} })`; + const refImports = overrideNames.length + ? [ + { + names: overrideNames, + from: this.getRelativeRefPath('crud', crudName), + }, + ] + : []; + + return { kind: 'crud', call, refImports }; } } diff --git a/packages/swarm-wasp/src/generators/crud/schema.ts b/packages/swarm-wasp/src/generators/crud/schema.ts index 22b5fa31..b896c92b 100644 --- a/packages/swarm-wasp/src/generators/crud/schema.ts +++ b/packages/swarm-wasp/src/generators/crud/schema.ts @@ -1,9 +1,9 @@ import { registerSchemaMetadata, SchemaManager } from '@ingenyus/swarm'; import { z } from 'zod/v4'; import { + CRUD_OPERATIONS, commonFieldMetadata, commonSchemas, - CRUD_OPERATIONS, } from '../../common'; const validCrudOperations = Object.values(CRUD_OPERATIONS); diff --git a/packages/swarm-wasp/src/generators/crud/templates/config/crud.eta b/packages/swarm-wasp/src/generators/crud/templates/config/crud.eta deleted file mode 100644 index d85c21bb..00000000 --- a/packages/swarm-wasp/src/generators/crud/templates/config/crud.eta +++ /dev/null @@ -1,4 +0,0 @@ - .addCrud(feature, "<%=crudName%>", { - entity: "<%=dataType%>", - <%=operations%>, - }) diff --git a/packages/swarm-wasp/src/generators/feature/feature-generator.test.ts b/packages/swarm-wasp/src/generators/feature/feature-generator.test.ts index 69845178..e5534eec 100644 --- a/packages/swarm-wasp/src/generators/feature/feature-generator.test.ts +++ b/packages/swarm-wasp/src/generators/feature/feature-generator.test.ts @@ -15,15 +15,21 @@ describe('WaspFeatureGenerator', () => { }); }); - it('generate creates feature directory structure', async () => { + it('generate creates feature directory structure and regenerates the barrel', async () => { fs.existsSync = vi.fn((p) => !p.includes('notfound')); fs.mkdirSync = vi.fn(); fs.copyFileSync = vi.fn(); + // The barrel generator scans src/features; return no existing features. + fs.readdirSync = vi.fn(() => []); + fs.writeFileSync = vi.fn(); await gen.generate({ target: 'test-feature' }); - // Since copyDirectory is mocked, we expect the generate method to complete successfully - // The actual copyDirectory call is internal to the generator expect(fs.existsSync).toHaveBeenCalled(); + // The features barrel (src/features/index.wasp.ts) is (re)written. + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining('index.wasp.ts'), + expect.stringContaining('featureSpecs') + ); }); }); diff --git a/packages/swarm-wasp/src/generators/feature/feature-generator.ts b/packages/swarm-wasp/src/generators/feature/feature-generator.ts index b0e4cfac..d0daab3a 100644 --- a/packages/swarm-wasp/src/generators/feature/feature-generator.ts +++ b/packages/swarm-wasp/src/generators/feature/feature-generator.ts @@ -1,5 +1,9 @@ -import { GeneratorServices, handleFatalError, Out } from '@ingenyus/swarm'; import path from 'node:path'; +import { + type GeneratorServices, + handleFatalError, + type Out, +} from '@ingenyus/swarm'; import { findWaspRoot, normaliseFeaturePath, @@ -57,6 +61,7 @@ export class FeatureGenerator extends WaspGeneratorBase { this.fileSystem.mkdirSync(featureDir, { recursive: true }); this.configGenerator.generate(normalisedPath); + this.barrelGenerator.regenerate(); this.logger.success(`Generated feature: ${normalisedPath}`); } } diff --git a/packages/swarm-wasp/src/generators/feature/templates/feature.wasp.eta b/packages/swarm-wasp/src/generators/feature/templates/feature.wasp.eta index 014e8669..71d02b39 100644 --- a/packages/swarm-wasp/src/generators/feature/templates/feature.wasp.eta +++ b/packages/swarm-wasp/src/generators/feature/templates/feature.wasp.eta @@ -1,5 +1,3 @@ -import { App } from "@ingenyus/swarm-wasp"; +import { type Spec } from '@wasp.sh/spec'; -export default function configureFeature(app: App, feature: string): void { - app -} +export const spec: Spec = []; diff --git a/packages/swarm-wasp/src/generators/job/job-generator.test.ts b/packages/swarm-wasp/src/generators/job/job-generator.test.ts index 32e42532..e1507736 100644 --- a/packages/swarm-wasp/src/generators/job/job-generator.test.ts +++ b/packages/swarm-wasp/src/generators/job/job-generator.test.ts @@ -1,115 +1,36 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { DEFAULT_CUSTOM_TEMPLATES_DIR } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { JobGenerator } from './job-generator'; import { schema } from './schema'; -// Mock getConfigManager -vi.mock('@ingenyus/swarm', async () => { - const actual = await vi.importActual('@ingenyus/swarm'); - return { - ...actual, - getConfigManager: vi.fn().mockImplementation(() => ({ - loadConfig: vi.fn().mockResolvedValue({ - templateDirectory: DEFAULT_CUSTOM_TEMPLATES_DIR, - plugins: [ - { - from: '@ingenyus/swarm-wasp', - import: 'wasp', - }, - ], - }), - })), - }; -}); - describe('JobGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: JobGenerator; beforeEach(async () => { - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(JobGenerator, schema, { - fileSystem: fs, - }); + gen = await createTestGenerator(JobGenerator, schema); }); - it('generate writes worker file and updates config', async () => { - fs.existsSync = vi.fn((p) => !p.includes('notfound')); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return '<%=imports%>\nexport const <%=jobName%>: <%=jobType%> = async (_args, _context) => {\n // TODO: Implement job logic\n console.log("Job executed");\n}'; - }); - fs.writeFileSync = vi.fn(); - - // Create generator after setting up mocks - gen = await createTestGenerator(JobGenerator, schema, { - fileSystem: fs, - }); + it('getDefinition builds a native job declaration with a schedule', () => { + const decl = gen.getDefinition('cleanup', ['Task'], '0 0 * * *', '{}'); - // Mock the template utility to return a simple template - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - if (templatePath.includes('config/job.eta')) { - return `app.addJob("${replacements.jobName}", { - schedule: "${replacements.cron}", - args: ${replacements.args} -});`; - } - return `// Generated job template for ${replacements.jobName || 'unknown'}`; - }), - resolveTemplatePath: vi.fn( - (templateName, generatorName, currentFileUrl) => { - return `/mock/templates/${generatorName}/templates/${templateName}`; - } - ), - }; - - await gen.generate({ - feature: 'foo', - name: 'Job', - force: true, - entities: ['User'], - cron: '0 0 * * *', - args: '{}', - }); - expect(fs.writeFileSync).toHaveBeenCalled(); - // The WaspBaseGenerator uses its own configGenerator instead of updateFeatureConfig - // So we expect the config file to be written directly - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('feature.wasp.ts'), - expect.any(String) + expect(decl.kind).toBe('job'); + expect(decl.call).toBe( + 'job(cleanup, { executor: "PgBoss", entities: ["Task"], schedule: { cron: "0 0 * * *" } })' ); + expect(decl.refImports).toEqual([ + { names: ['cleanup'], from: './server/jobs/cleanup' }, + ]); }); - it('getDefinition returns processed template', () => { - // Mock the template utility to process templates - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - return `testJob: { schedule: "", args: {} }`; - }), - resolveTemplatePath: vi.fn( - (templateName, generatorName, currentFileUrl) => { - return `/mock/templates/${generatorName}/templates/${templateName}`; - } - ), - }; + it('omits schedule when no cron is given and includes schedule args when provided', () => { + expect(gen.getDefinition('cleanup', [], '', '{}').call).toBe( + 'job(cleanup, { executor: "PgBoss" })' + ); - const result = gen.getDefinition('testJob', [], '', '{}'); - expect(typeof result).toBe('string'); + expect( + gen.getDefinition('cleanup', [], '0 0 * * *', '{ limit: 10 }').call + ).toBe( + 'job(cleanup, { executor: "PgBoss", schedule: { cron: "0 0 * * *", args: { limit: 10 } } })' + ); }); }); diff --git a/packages/swarm-wasp/src/generators/job/job-generator.ts b/packages/swarm-wasp/src/generators/job/job-generator.ts index 46ecbd32..272eeff5 100644 --- a/packages/swarm-wasp/src/generators/job/job-generator.ts +++ b/packages/swarm-wasp/src/generators/job/job-generator.ts @@ -1,11 +1,7 @@ -import { - capitalise, - GeneratorServices, - Out, - toCamelCase, -} from '@ingenyus/swarm'; +import { capitalise, type Out, toCamelCase } from '@ingenyus/swarm'; import { CONFIG_TYPES } from '../../common'; import { ComponentGeneratorBase } from '../base'; +import type { SpecDeclaration } from '../config'; import { schema } from './schema'; export class JobGenerator extends ComponentGeneratorBase< @@ -19,10 +15,6 @@ export class JobGenerator extends ComponentGeneratorBase< description = 'Generates a Wasp Job'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const jobName = toCamelCase(args.name); @@ -88,32 +80,33 @@ export class JobGenerator extends ComponentGeneratorBase< executionArgs ); - this.updateConfigWithCheck( - configPath, - 'job', - jobName, - definition, - featurePath, - force - ); + this.updateConfigWithCheck(configPath, definition, featurePath, force); } /** - * Generates a job definition for the feature configuration. + * Builds a native job spec declaration for the feature configuration. */ getDefinition( jobName: string, entities: string[], cron: string, args: string - ): string { - const templatePath = this.getDefaultTemplatePath('config/job.eta'); + ): SpecDeclaration { + const from = this.getRelativeRefPath('job', jobName); + const hasArgs = args && args !== '{}'; + const schedule = cron + ? `{ cron: "${cron}"${hasArgs ? `, args: ${args}` : ''} }` + : undefined; + const call = `job(${jobName}, ${this.configObject([ + 'executor: "PgBoss"', + entities.length ? `entities: ${this.stringArray(entities)}` : undefined, + schedule ? `schedule: ${schedule}` : undefined, + ])})`; - return this.templateUtility.processTemplate(templatePath, { - jobName, - entities: entities.map((e) => `"${e}"`).join(', '), - cron, - args, - }); + return { + kind: 'job', + call, + refImports: [{ names: [jobName], from }], + }; } } diff --git a/packages/swarm-wasp/src/generators/job/schema.ts b/packages/swarm-wasp/src/generators/job/schema.ts index 58ab50a9..5116c2d7 100644 --- a/packages/swarm-wasp/src/generators/job/schema.ts +++ b/packages/swarm-wasp/src/generators/job/schema.ts @@ -35,13 +35,14 @@ const cronSchema = z const [base, step] = item.split('/'); const stepNum = parseInt(step, 10); - if (isNaN(stepNum) || stepNum <= 0) return false; + if (Number.isNaN(stepNum) || stepNum <= 0) return false; if (base === '*') continue; // */15 is valid const baseNum = parseInt(base, 10); - if (isNaN(baseNum) || baseNum < min || baseNum > max) return false; + if (Number.isNaN(baseNum) || baseNum < min || baseNum > max) + return false; } else if (item.includes('-')) { // Handle ranges like 1-5 const [start, end] = item.split('-'); @@ -49,8 +50,8 @@ const cronSchema = z const endNum = parseInt(end, 10); if ( - isNaN(startNum) || - isNaN(endNum) || + Number.isNaN(startNum) || + Number.isNaN(endNum) || startNum < min || endNum > max || startNum > endNum @@ -60,7 +61,7 @@ const cronSchema = z // Handle single values const num = parseInt(item, 10); - if (isNaN(num) || num < min || num > max) return false; + if (Number.isNaN(num) || num < min || num > max) return false; } } diff --git a/packages/swarm-wasp/src/generators/job/templates/config/job.eta b/packages/swarm-wasp/src/generators/job/templates/config/job.eta deleted file mode 100644 index 710ee5f7..00000000 --- a/packages/swarm-wasp/src/generators/job/templates/config/job.eta +++ /dev/null @@ -1,11 +0,0 @@ - .addJob(feature, "<%=jobName%>", { -<%- if (cron !== '') { %> - cron: "<%=cron%>", -<%- } %> -<%- if (args !== '{}') { %> - args: <%=args%>, -<%- } %> -<%- if (entities.length > 0) { %> - entities: [<%=entities%>], -<%- } %> - }) diff --git a/packages/swarm-wasp/src/generators/query/query-generator.test.ts b/packages/swarm-wasp/src/generators/query/query-generator.test.ts index be04436a..d6cae724 100644 --- a/packages/swarm-wasp/src/generators/query/query-generator.test.ts +++ b/packages/swarm-wasp/src/generators/query/query-generator.test.ts @@ -1,665 +1,42 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { QueryGenerator } from './query-generator'; import { schema } from './schema'; -vi.mock('../../common', async () => { - const actual = await vi.importActual('../../common'); - return { - ...actual, - getEntityMetadata: vi.fn(), - getIdFields: vi.fn(), - getRequiredFields: vi.fn(), - getOptionalFields: vi.fn(), - getJsonFields: vi.fn(), - needsPrismaImport: vi.fn(), - generateJsonTypeHandling: vi.fn(), - generatePickType: vi.fn(), - generateOmitType: vi.fn(), - generatePartialType: vi.fn(), - generateIntersectionType: vi.fn(), - normaliseFeaturePath: vi.fn((path) => path), - getFeatureDir: vi.fn((fs, path) => `/test-project/src/features/${path}`), - getFeatureImportPath: vi.fn((path) => path), - ensureDirectoryExists: vi.fn(), - TemplateUtility: function MockTemplateUtility(this: any) { - this.processTemplate = vi.fn(); - this.resolveTemplatePath = vi.fn(); - }, - }; -}); - -vi.mock('../../common/prisma', async () => { - const actual = await vi.importActual('../../common/prisma'); - return { - ...actual, - getEntityMetadata: vi.fn(), - getIdFields: vi.fn(), - getRequiredFields: vi.fn(), - getOptionalFields: vi.fn(), - getJsonFields: vi.fn(), - needsPrismaImport: vi.fn(), - generateJsonTypeHandling: vi.fn(), - generatePickType: vi.fn(), - generateOmitType: vi.fn(), - generatePartialType: vi.fn(), - generateIntersectionType: vi.fn(), - }; -}); - describe('QueryGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: QueryGenerator; beforeEach(async () => { - const { - getEntityMetadata, - getIdFields, - getRequiredFields, - getOptionalFields, - getJsonFields, - needsPrismaImport, - generateJsonTypeHandling, - generatePickType, - generateOmitType, - generatePartialType, - generateIntersectionType, - } = await import('../../common'); - - (getEntityMetadata as any).mockResolvedValue({ - name: 'User', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'email', type: 'String', tsType: 'string', isRequired: true }, - { name: 'name', type: 'String', tsType: 'string', isRequired: false }, - ], - }); - (getIdFields as any).mockReturnValue(['id']); - (getRequiredFields as any).mockReturnValue(['email']); - (getOptionalFields as any).mockReturnValue(['name']); - (getJsonFields as any).mockReturnValue([]); - (needsPrismaImport as any).mockReturnValue(false); - (generateJsonTypeHandling as any).mockReturnValue(''); - (generatePickType as any).mockImplementation( - (modelName: string, fields: string[]) => - fields.length - ? `Pick<${modelName}, ${fields.map((f) => `"${f}"`).join(' | ')}>` - : '' - ); - (generateOmitType as any).mockImplementation( - (modelName: string, fields: string[]) => - fields.length - ? `Omit<${modelName}, ${fields.map((f) => `"${f}"`).join(' | ')}>` - : modelName - ); - (generatePartialType as any).mockImplementation((typeString: string) => - typeString ? `Partial<${typeString}>` : '' - ); - (generateIntersectionType as any).mockImplementation( - (type1: string, type2: string) => { - if (!type1 && !type2) return ''; - if (!type1) return type2; - if (!type2) return type1; - return `${type1} & ${type2}`; - } - ); - - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - }); - - it('generate writes query file and updates config', async () => { - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; // config file exists - if (typeof p === 'string' && p.endsWith('.ts')) return false; // query file does not exist - if (typeof p === 'string' && p.includes('queries')) return false; // query dir does not exist - if (typeof p === 'string' && p.includes('schema.prisma')) return true; // Prisma schema exists - return true; // all others exist - }); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('schema.prisma')) { - return `model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -}`; - } - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - fs.copyFileSync = vi.fn(); - fs.mkdirSync = vi.fn(); - - // Create generator after setting up mocks - gen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - // Mock the template utility to return a simple template - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Config template - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addQuery(feature, "${replacements.operationName}", { - entities: [], - auth: false, - })`; - } - // Operation template - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }), - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/query/templates/get.eta`; - } - ), - }; - - await gen.generate({ - feature: 'foo', - dataType: 'User', - operation: 'get', - entities: ['User'], - force: true, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - // The WaspBaseGenerator uses its own configGenerator instead of updateFeatureConfig - // So we expect the config file to be written directly - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('feature.wasp.ts'), - expect.any(String) - ); + gen = await createTestGenerator(QueryGenerator, schema); }); - it('handles getAll query', async () => { - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('queries')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('schema.prisma')) { - return `model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -}`; - } - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - gen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Config template - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addQuery(feature, "${replacements.operationName}", { - entities: [], - auth: false, - })`; - } - // Operation template - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }), - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/query/templates/getAll.eta`; - } - ), - }; - - await gen.generate({ - feature: 'bar', - dataType: 'User', - operation: 'getAll', - entities: ['User'], - force: true, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - }); - - it('handles getFiltered query', async () => { - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('queries')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.includes('schema.prisma')) { - return `model User { - id Int @id @default(autoincrement()) - email String @unique - name String? -}`; - } - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - gen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - // Config template - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addQuery(feature, "${replacements.operationName}", { - entities: [], - auth: false, - })`; - } - // Operation template - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }), - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/query/templates/getFiltered.eta`; - } - ), - }; - - await gen.generate({ - feature: 'baz', - dataType: 'User', - operation: 'getFiltered', - entities: ['User'], - force: true, - }); - - expect(fs.writeFileSync).toHaveBeenCalled(); - }); - - it('automatically includes dataType in entities array when not specified', async () => { - const { getEntityMetadata } = await import('../../common'); - (getEntityMetadata as any).mockResolvedValue({ - name: 'Task', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'name', type: 'String', tsType: 'string', isRequired: true }, - ], - }); - - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('queries')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - const testGen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addQuery(feature, "${replacements.operationName}", { - entities: ${replacements.entities}, - auth: false, - })`; - } - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }); - - (testGen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/query/templates/get.eta`; - } - ), - }; - - await testGen.generate({ - feature: 'tasks', - dataType: 'Task', - operation: 'get', - force: true, - }); - - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - entities: '"Task"', - }) - ); - }); - - it('prevents duplicate dataType in entities array', async () => { - const { getEntityMetadata } = await import('../../common'); - (getEntityMetadata as any).mockResolvedValue({ - name: 'Task', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'name', type: 'String', tsType: 'string', isRequired: true }, - ], - }); - - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('queries')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - const testGen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addQuery(feature, "${replacements.operationName}", { - entities: ${replacements.entities}, - auth: false, - })`; - } - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }); - - (testGen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/query/templates/get.eta`; - } - ), - }; - - await testGen.generate({ - feature: 'tasks', - dataType: 'Task', - operation: 'get', - entities: ['Task'], - force: true, - }); - - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - entities: '"Task"', - }) - ); - }); - - it('places dataType first in entities array with other entities', async () => { - const { getEntityMetadata } = await import('../../common'); - (getEntityMetadata as any).mockResolvedValue({ - name: 'Task', - fields: [ - { - name: 'id', - type: 'Int', - tsType: 'number', - isId: true, - isRequired: true, - hasDefaultValue: true, - }, - { name: 'name', type: 'String', tsType: 'string', isRequired: true }, - ], - }); - - fs.existsSync = vi.fn((p) => { - if (typeof p === 'string' && p.endsWith('.wasp.ts')) return true; - if (typeof p === 'string' && p.endsWith('.ts')) return false; - if (typeof p === 'string' && p.includes('queries')) return false; - if (typeof p === 'string' && p.includes('schema.prisma')) return true; - return true; - }); - fs.mkdirSync = vi.fn(); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'export const <%=operationName%> = async (args: any) => { return {}; }'; - }); - fs.writeFileSync = vi.fn(); - - const testGen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - const mockProcessTemplate = vi.fn((templatePath, replacements) => { - if ( - templatePath.includes('/config/') && - templatePath.includes('operation.eta') - ) { - return ` .addQuery(feature, "${replacements.operationName}", { - entities: ${replacements.entities}, - auth: false, - })`; - } - return `export const ${replacements.operationName || 'unknownOperation'} = async (args: any) => { return {}; }`; - }); - - (testGen as any).templateUtility = { - processTemplate: mockProcessTemplate, - resolveTemplatePath: vi.fn( - (templateName, _generatorName, _currentFileUrl) => { - if (templateName === 'operation.eta') { - return `/mock/templates/config/operation.eta`; - } - return `/mock/templates/query/templates/get.eta`; - } - ), - }; - - await testGen.generate({ - feature: 'tasks', - dataType: 'Task', - operation: 'get', - entities: ['User', 'Product'], - force: true, - }); - - expect(mockProcessTemplate).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - entities: '"Task", "User", "Product"', - }) + it('getOperationDefinition builds a native query declaration', () => { + const decl = gen.getOperationDefinition( + 'getTask', + ['Task'], + 'query', + false ); - }); - - it('getDefinition throws error when config template file does not exist', async () => { - gen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - // Mock resolveTemplatePath to return a non-existent path - const nonExistentPath = '/non/existent/path/operation.eta'; - (gen as any).templateUtility = { - resolveTemplatePath: vi.fn(() => nonExistentPath), - processTemplate: vi.fn((templatePath) => { - // Simulate file not found error - Eta will throw if template doesn't exist - throw new Error(`Template file not found: ${templatePath}`); - }), - }; - - // Mock fs.existsSync to return false for the template path - fs.existsSync = vi.fn((path) => { - if (path === nonExistentPath) { - return false; - } - return true; - }); - - expect(() => { - gen.getDefinition( - 'testQuery', - 'test', - ['User'], - 'query', - 'features/test/server/queries/testQuery', - false - ); - }).toThrow(/Template file not found/); - - // Verify resolveTemplatePath was called with correct parameters - expect( - (gen as any).templateUtility.resolveTemplatePath - ).toHaveBeenCalledWith( - 'operation.eta', - 'config', - expect.any(String) // import.meta.url + expect(decl.kind).toBe('query'); + expect(decl.call).toBe( + 'query(getTask, { entities: ["Task"], auth: false })' ); + expect(decl.refImports).toEqual([ + { names: ['getTask'], from: './server/queries/getTask' }, + ]); }); - it('getDefinition verifies config template path resolution', async () => { - gen = await createTestGenerator(QueryGenerator, schema, { - fileSystem: fs, - }); - - const mockResolveTemplatePath = vi.fn((templateName, generatorName) => { - if (templateName === 'operation.eta' && generatorName === 'config') { - return '/mock/templates/config/operation.eta'; - } - return `/mock/templates/${templateName}`; - }); - - (gen as any).templateUtility = { - resolveTemplatePath: mockResolveTemplatePath, - processTemplate: vi.fn(() => 'app.addQuery("test", {});'), - }; - - fs.existsSync = vi.fn(() => true); - - await gen.getDefinition( - 'testQuery', - 'test', - ['User'], + it('includes multiple entities in declaration order', () => { + const decl = gen.getOperationDefinition( + 'getTasks', + ['Task', 'Tag'], 'query', - 'features/test/server/queries/testQuery', - false + true ); - // Verify resolveTemplatePath was called with correct parameters for config template - expect(mockResolveTemplatePath).toHaveBeenCalledWith( - 'operation.eta', - 'config', - expect.any(String) // import.meta.url + expect(decl.call).toBe( + 'query(getTasks, { entities: ["Task", "Tag"], auth: true })' ); }); }); diff --git a/packages/swarm-wasp/src/generators/query/query-generator.ts b/packages/swarm-wasp/src/generators/query/query-generator.ts index b4083d54..41666bfa 100644 --- a/packages/swarm-wasp/src/generators/query/query-generator.ts +++ b/packages/swarm-wasp/src/generators/query/query-generator.ts @@ -1,4 +1,4 @@ -import { GeneratorServices, Out } from '@ingenyus/swarm'; +import type { Out } from '@ingenyus/swarm'; import { CONFIG_TYPES } from '../../common'; import { OperationGeneratorBase } from '../base'; import { schema } from './schema'; @@ -14,10 +14,6 @@ export class QueryGenerator extends OperationGeneratorBase< description = 'Generates a Wasp Query'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const { dataType, feature, name } = args; const operation = args.operation; @@ -46,9 +42,10 @@ export class QueryGenerator extends OperationGeneratorBase< this.ensureWaspCompatible(); const configPath = this.validateFeatureConfig(feature); - const { targetDirectory: operationsDir, importDirectory } = - this.ensureTargetDirectory(feature, operationType); - const importPath = `${importDirectory}/${operationName}`; + const { targetDirectory: operationsDir } = this.ensureTargetDirectory( + feature, + operationType + ); this.generateOperationFile( operationsDir, @@ -58,19 +55,15 @@ export class QueryGenerator extends OperationGeneratorBase< ); // Generate config definition and update - const definition = this.getDefinition( + const definition = this.getOperationDefinition( operationName, - feature, entities, 'query', - importPath, args.auth ); this.updateConfigWithCheck( configPath, - 'addQuery', - operationName, definition, feature, args.force || false diff --git a/packages/swarm-wasp/src/generators/route/route-generator.test.ts b/packages/swarm-wasp/src/generators/route/route-generator.test.ts index 174119de..e8199cca 100644 --- a/packages/swarm-wasp/src/generators/route/route-generator.test.ts +++ b/packages/swarm-wasp/src/generators/route/route-generator.test.ts @@ -1,111 +1,30 @@ -import type { FileSystem, Generator } from '@ingenyus/swarm'; -import { DEFAULT_CUSTOM_TEMPLATES_DIR } from '@ingenyus/swarm'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - createMockFeatureGen, - createMockFS, - createTestGenerator, -} from '../../../tests/utils'; -import { schema as featureSchema } from '../feature/schema'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { createTestGenerator } from '../../../tests/utils'; import { RouteGenerator } from './route-generator'; import { schema } from './schema'; -// Mock getConfigManager -vi.mock('@ingenyus/swarm', async () => { - const actual = await vi.importActual('@ingenyus/swarm'); - return { - ...actual, - getConfigManager: vi.fn().mockImplementation(() => ({ - loadConfig: vi.fn().mockResolvedValue({ - templateDirectory: DEFAULT_CUSTOM_TEMPLATES_DIR, - plugins: [ - { - from: '@ingenyus/swarm-wasp', - import: 'wasp', - }, - ], - }), - })), - }; -}); - describe('RouteGenerator', () => { - let fs: FileSystem; - let featureGen: Generator; let gen: RouteGenerator; beforeEach(async () => { - fs = createMockFS(); - featureGen = createMockFeatureGen(featureSchema); - gen = await createTestGenerator(RouteGenerator, schema, { - fileSystem: fs, - }); + gen = await createTestGenerator(RouteGenerator, schema); }); - it('generate writes route file and updates config', async () => { - fs.existsSync = vi.fn((p) => !p.includes('notfound')); - fs.readFileSync = vi.fn((path) => { - if (typeof path === 'string' && path.endsWith('.wasp.ts')) { - return `import { App } from "@ingenyus/swarm-wasp"; - -export default function configureFeature(app: App, feature: string): void { - app -}`; - } - return 'import React from "react";\n\nexport const <%=componentName%> = () => {\n return (\n
\n

<%=displayName%>

\n {/* TODO: Add page content */}\n
\n );\n};'; - }); - fs.writeFileSync = vi.fn(); - - // Create generator after setting up mocks - gen = await createTestGenerator(RouteGenerator, schema, { - fileSystem: fs, - }); - - // Mock the template utility to return a simple template - (gen as any).templateUtility = { - processTemplate: vi.fn((templatePath, replacements) => { - if (templatePath.includes('route.eta')) { - return `app.addPage("${replacements.routeName}", { - path: "${replacements.routePath}", - component: "${replacements.componentName}" -});`; - } - return `// Generated route template for ${replacements.componentName || 'unknown'}`; - }), - resolveTemplatePath: vi.fn( - (templateName, generatorName, currentFileUrl) => { - return `/mock/templates/${generatorName}/templates/${templateName}`; - } - ), - }; + it('getDefinition builds a native route declaration with an auth-protected page', () => { + const decl = gen.getDefinition('dashboard', '/dashboard', true); - await gen.generate({ - feature: 'foo', - name: 'route', - path: '/foo', - force: true, - }); - expect(fs.writeFileSync).toHaveBeenCalled(); - // The WaspBaseGenerator uses its own configGenerator instead of updateFeatureConfig - // So we expect the config file to be written directly - expect(fs.writeFileSync).toHaveBeenCalledWith( - expect.stringContaining('feature.wasp.ts'), - expect.any(String) + expect(decl.kind).toBe('route'); + expect(decl.call).toBe( + 'route("dashboard", "/dashboard", page(Dashboard, { authRequired: true }))' ); + expect(decl.refImports).toEqual([ + { names: ['Dashboard'], from: './client/pages/Dashboard' }, + ]); }); - it('getDefinition returns processed template', () => { - // Mock the template utility - (gen as any).templateUtility = { - processTemplate: vi.fn( - () => 'app.addRoute("testRoute", { path: "/test", component: "test" });' - ), - resolveTemplatePath: vi.fn( - (templateName) => `/mock/templates/${templateName}` - ), - }; + it('omits authRequired when auth is false', () => { + const decl = gen.getDefinition('dashboard', '/dashboard', false); - const result = gen.getDefinition('testRoute', '/test', 'test', false); - expect(typeof result).toBe('string'); + expect(decl.call).toBe('route("dashboard", "/dashboard", page(Dashboard))'); }); }); diff --git a/packages/swarm-wasp/src/generators/route/route-generator.ts b/packages/swarm-wasp/src/generators/route/route-generator.ts index 69aa09a0..cd0d3344 100644 --- a/packages/swarm-wasp/src/generators/route/route-generator.ts +++ b/packages/swarm-wasp/src/generators/route/route-generator.ts @@ -1,12 +1,12 @@ import { formatDisplayName, - GeneratorServices, - Out, + type Out, toCamelCase, toPascalCase, } from '@ingenyus/swarm'; import { CONFIG_TYPES, getRouteNameFromPath } from '../../common'; import { ComponentGeneratorBase } from '../base'; +import type { SpecDeclaration } from '../config'; import { schema } from './schema'; export class RouteGenerator extends ComponentGeneratorBase< @@ -20,10 +20,6 @@ export class RouteGenerator extends ComponentGeneratorBase< description = 'Generates a Wasp Page and Route'; schema = schema; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { const { path: routePath, name, feature } = args; const routeName = toCamelCase(name || getRouteNameFromPath(routePath)); @@ -51,7 +47,7 @@ export class RouteGenerator extends ComponentGeneratorBase< componentName: string, args: Out ) { - const templatePath = 'files/client/page.eta'; + const _templatePath = 'files/client/page.eta'; const replacements = { componentName, displayName: formatDisplayName(componentName), @@ -73,17 +69,10 @@ export class RouteGenerator extends ComponentGeneratorBase< args: Out, configPath: string ) { - const definition = this.getDefinition( - routeName, - routePath, - featurePath, - args.auth - ); + const definition = this.getDefinition(routeName, routePath, args.auth); this.updateConfigWithCheck( configPath, - 'addRoute', - routeName, definition, featurePath, args.force || false @@ -91,21 +80,23 @@ export class RouteGenerator extends ComponentGeneratorBase< } /** - * Generates a route definition for the feature configuration. + * Builds a native route spec declaration for the feature configuration. */ getDefinition( routeName: string, routePath: string, - featurePath: string, auth = false - ): string { - const templatePath = this.getDefaultTemplatePath('config/route.eta'); + ): SpecDeclaration { + const componentName = toPascalCase(routeName); + const from = this.getRelativeRefPath('page', componentName); + const pageArgs = auth + ? `${componentName}, { authRequired: true }` + : componentName; - return this.templateUtility.processTemplate(templatePath, { - featureName: featurePath.split('/').pop() || featurePath, - routeName, - routePath, - auth: String(auth), - }); + return { + kind: 'route', + call: `route("${routeName}", "${routePath}", page(${pageArgs}))`, + refImports: [{ names: [componentName], from }], + }; } } diff --git a/packages/swarm-wasp/src/generators/route/templates/config/route.eta b/packages/swarm-wasp/src/generators/route/templates/config/route.eta deleted file mode 100644 index ad6bd0e3..00000000 --- a/packages/swarm-wasp/src/generators/route/templates/config/route.eta +++ /dev/null @@ -1,4 +0,0 @@ - .addRoute(feature, "<%=routeName%>", { - path: "<%=routePath%>", - auth: <%=auth%>, - }) diff --git a/packages/swarm-wasp/src/generators/shared/templates/middleware/middleware.eta b/packages/swarm-wasp/src/generators/shared/templates/middleware/middleware.eta index 4f6f50b0..33a1a194 100644 --- a/packages/swarm-wasp/src/generators/shared/templates/middleware/middleware.eta +++ b/packages/swarm-wasp/src/generators/shared/templates/middleware/middleware.eta @@ -2,12 +2,12 @@ import type { RequestHandler } from "express"; import type { MiddlewareConfigFn } from "wasp/server"; export const <%=name%>: MiddlewareConfigFn = (middlewareConfig) => { - const <%=name%>Middleware: RequestHandler = (_req, _res, next) => { + const handler: RequestHandler = (_req, _res, next) => { // TODO: Add your custom logic here next(); }; - middlewareConfig.set("<%=name%>.middleware", <%=name%>Middleware); + middlewareConfig.set("<%=name%>", handler); return middlewareConfig; }; diff --git a/packages/swarm-wasp/src/index.ts b/packages/swarm-wasp/src/index.ts index b2ceffbe..7fb5b098 100644 --- a/packages/swarm-wasp/src/index.ts +++ b/packages/swarm-wasp/src/index.ts @@ -1,4 +1,2 @@ export * from './generators'; export * from './plugin'; -export * from './wasp-config'; -export { App } from './wasp-config/app'; diff --git a/packages/swarm-wasp/src/wasp-config/app.ts b/packages/swarm-wasp/src/wasp-config/app.ts deleted file mode 100644 index 087784f3..00000000 --- a/packages/swarm-wasp/src/wasp-config/app.ts +++ /dev/null @@ -1,532 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; - -// Import types and classes from wasp-config -// In development: resolves to stub implementation via devDependency link -// In production (Wasp project): resolves to real wasp-config package -import { - ActionConfig, - ApiConfig, - ApiNamespaceConfig, - AppConfig, - AuthConfig, - ClientConfig, - CrudConfig, - DbConfig, - EmailSenderConfig, - JobConfig, - QueryConfig, - RouteConfig, - App as WaspApp, -} from 'wasp-config'; -import { ApiHttpMethod } from '../common'; - -// Type definitions for helper method options -interface RouteOptions { - path: string; - auth?: boolean; -} - -interface ApiOptions { - method: ApiHttpMethod; - route: string; - customMiddleware?: boolean; - entities?: string[]; - auth?: boolean; -} - -interface CrudOperationOptions { - entities?: string[]; - isPublic?: boolean; - override?: boolean; -} - -interface CrudOptions { - entity: string; - getAll?: CrudOperationOptions; - get?: CrudOperationOptions; - create?: CrudOperationOptions; - update?: CrudOperationOptions; - delete?: CrudOperationOptions; -} - -interface OperationOptions { - entities?: string[]; - auth?: boolean; -} - -interface JobOptions { - entities?: string[]; - cron?: string; - args?: Record; -} - -interface ApiNamespaceOptions { - path: string; -} - -/** - * Enhanced Wasp App class with Swarm-specific functionality - * - * This class extends the default Wasp App class with: - * - Chainable helper methods for simplified configuration - * - Dynamic feature-based configuration loading - * - Enhanced error handling and validation - */ -export class App extends WaspApp { - constructor(name: string, config: AppConfig) { - super(name, config); - } - - /** - * Static factory method that creates and initializes Swarm with configuration - * dynamically loaded from feature directories - * - * @param name The name of the application - * @param config The base configuration for the application - * @returns An initialized Swarm instance - */ - static async create(name: string, config: AppConfig): Promise { - const app = new App(name, config); - - await app.configureFeatures(); - - return app; - } - - // Chainable configuration methods - public auth(authConfig: AuthConfig): this { - super.auth(authConfig); - - return this; - } - - public client(clientConfig: ClientConfig): this { - super.client(clientConfig); - - return this; - } - - public db(dbConfig: DbConfig): this { - super.db(dbConfig); - - return this; - } - - public emailSender(emailSenderConfig: EmailSenderConfig): this { - super.emailSender(emailSenderConfig); - - return this; - } - - public job(name: string, jobConfig: JobConfig): this { - super.job(name, jobConfig); - - return this; - } - - public query(name: string, queryConfig: QueryConfig): this { - super.query(name, queryConfig); - - return this; - } - - public route(name: string, routeConfig: RouteConfig): this { - super.route(name, routeConfig); - - return this; - } - - public api(name: string, apiConfig: ApiConfig): this { - super.api(name, apiConfig); - - return this; - } - - public apiNamespace( - name: string, - apiNamespaceConfig: ApiNamespaceConfig - ): this { - super.apiNamespace(name, apiNamespaceConfig); - - return this; - } - - public crud(name: string, crudConfig: CrudConfig): this { - super.crud(name, crudConfig); - - return this; - } - - public action(name: string, actionConfig: ActionConfig): this { - super.action(name, actionConfig); - - return this; - } - - /** - * Helper method to add routes with simplified parameters - * @param featureName The name of the feature - * @param name Route name, e.g. "DashboardRoute" - * @param options Route configuration options - */ - public addRoute( - featureName: string, - name: string, - options: RouteOptions - ): this { - const componentName = name.charAt(0).toUpperCase() + name.slice(1); - const importPath = this.getFeatureImportPath( - featureName, - 'client', - 'pages', - componentName - ); - const routeConfig = { - path: options.path, - to: this.page(componentName, { - authRequired: options.auth || false, - component: { - import: componentName, - from: `@src/${importPath}`, - }, - }), - }; - - super.route(name, routeConfig); - - return this; - } - - /** - * Helper method to add API endpoints with simplified parameters - * @param featureName The name of the feature - * @param name API endpoint name, e.g. "getTasksApi" - * @param options API configuration options - */ - public addApi(featureName: string, name: string, options: ApiOptions): this { - const importPath = this.getFeatureImportPath( - featureName, - 'server', - 'apis', - name - ); - const middlewareImportPath = this.getFeatureImportPath( - featureName, - 'server', - path.join('apis', 'middleware'), - name - ); - - super.api(name, { - fn: { - import: name, - from: `@src/${importPath}`, - }, - ...(options.customMiddleware && { - import: name, - from: `@src/${middlewareImportPath}`, - }), - entities: options.entities, - httpRoute: { method: options.method, route: options.route }, - auth: options.auth || false, - }); - - return this; - } - - /** - * Helper method to add CRUD operations with simplified parameters - * @param featureName The name of the feature - * @param name The CRUD name - * @param options CRUD configuration options - */ - public addCrud( - featureName: string, - name: string, - options: CrudOptions - ): this { - const processOperationOptions = ( - operationName: string, - operationOptions?: CrudOperationOptions - ) => { - if (!operationOptions) return undefined; - - const processedOptions: any = { ...operationOptions }; - - if (operationOptions.override) { - const operationDataType = - operationName === 'getAll' - ? this.getPlural(options.entity) - : options.entity; - const operationComponent = `${operationName}${operationDataType}`; - const importPath = this.getFeatureImportPath( - featureName, - 'server', - 'cruds', - name.charAt(0).toLowerCase() + name.slice(1) - ); - - processedOptions.overrideFn = { - import: operationComponent, - from: `@src/${importPath}`, - }; - delete processedOptions.override; - } - - return processedOptions; - }; - - super.crud(this.getPlural(options.entity), { - entity: options.entity, - operations: { - getAll: processOperationOptions('getAll', options.getAll), - get: processOperationOptions('get', options.get), - create: processOperationOptions('create', options.create), - update: processOperationOptions('update', options.update), - delete: processOperationOptions('delete', options.delete), - }, - }); - - return this; - } - - /** - * Helper method to add actions with simplified parameters - * @param featureName The name of the feature - * @param name The action name - * @param options Action configuration options - */ - public addAction( - featureName: string, - name: string, - options: OperationOptions - ): this { - const importPath = this.getFeatureImportPath( - featureName, - 'server', - 'actions', - name - ); - const config = this.getOperationConfig( - name, - importPath, - options.entities, - options.auth - ); - - super.action(name, config); - - return this; - } - - /** - * Helper method to add queries with simplified parameters - * @param featureName The name of the feature - * @param name The query name - * @param options Query configuration options - */ - public addQuery( - featureName: string, - name: string, - options: OperationOptions - ): this { - const importPath = this.getFeatureImportPath( - featureName, - 'server', - 'queries', - name - ); - const config = this.getOperationConfig( - name, - importPath, - options.entities, - options.auth - ); - - super.query(name, config); - - return this; - } - - /** - * Helper method to add background jobs with simplified parameters - * @param featureName The name of the feature - * @param name Job name - * @param options Job configuration options - */ - public addJob(featureName: string, name: string, options: JobOptions): this { - const importPath = this.getFeatureImportPath( - featureName, - 'server', - 'jobs', - name - ); - - super.job(name, { - executor: 'PgBoss', - perform: { - fn: { - import: name, - from: `@src/${importPath}`, - }, - }, - entities: options.entities, - ...(options.cron && { - schedule: { - cron: options.cron, - args: options.args || {}, - }, - }), - }); - - return this; - } - - /** - * Helper method to add API namespaces with simplified parameters - * @param featureName The name of the feature - * @param name Namespace name - * @param options API namespace configuration options - */ - public addApiNamespace( - featureName: string, - name: string, - options: ApiNamespaceOptions - ): this { - const importPath = this.getFeatureImportPath( - featureName, - 'server', - path.join('apis', 'middleware'), - name - ); - - super.apiNamespace(name, { - path: options.path, - middlewareConfigFn: { - import: name, - from: `@src/${importPath}`, - }, - }); - - return this; - } - - /** - * Calculates the import path for a feature component - * @param featureName The name of the feature - * @param type The type of component (client, server, etc.) - * @param subPath The sub-path within the feature directory - * @param fileName The name of the file (optional, defaults to featureName) - * @returns The calculated import path - */ - private getFeatureImportPath( - featureName: string, - type: 'client' | 'server', - subPath: string, - fileName?: string - ): string { - const file = fileName || featureName; - - return `features/${featureName}/${type}/${subPath}/${file}`; - } - - /** - * Converts a singular word to its plural form - * @param word The singular word to pluralize - * @returns The plural form of the word - */ - private getPlural(word: string): string { - if (word.endsWith('y')) { - return word.slice(0, -1) + 'ies'; - } else if ( - word.endsWith('s') || - word.endsWith('sh') || - word.endsWith('ch') || - word.endsWith('x') || - word.endsWith('z') - ) { - return word + 'es'; - } else { - return word + 's'; - } - } - - /** - * Configures all feature modules by scanning the features directory - */ - async configureFeatures(): Promise { - const featuresDir = path.join(process.cwd(), 'src', 'features'); - - // Check if features directory exists - if (!fs.existsSync(featuresDir)) { - console.warn( - 'Features directory not found, skipping feature configuration' - ); - return this; - } - - const getAllFeatureFiles = (dir: string): string[] => { - let results: string[] = []; - const list = fs.readdirSync(dir, { withFileTypes: true }); - - for (const entry of list) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - results = results.concat(getAllFeatureFiles(fullPath)); - } else if (entry.isFile() && entry.name.endsWith('.wasp.ts')) { - results.push(path.relative(featuresDir, fullPath)); - } - } - return results; - }; - - const featureFiles = getAllFeatureFiles(featuresDir); - - for (const file of featureFiles) { - try { - const featureName = path.dirname(file); - const modulePath = path.join( - process.cwd(), - '.wasp', - 'src', - 'features', - file.replace('.ts', '.js') - ); - const module = await import(modulePath); - - if (module.default) { - module.default(this, featureName); - } - } catch (error) { - console.error(`Failed to load feature module ${file}:`, error); - } - } - - return this; - } - - /** - * Helper method to get the configuration for an action or query - * @param name The operation name - * @param importPath Import path (excluding `@src/` prefix), e.g. "features/dashboard/server/queries/getTasks" - * @param entities Comma-separated list of entities (optional, defaults to datatype) - * @param auth Require authentication (optional) - */ - private getOperationConfig( - name: string, - importPath: string, - entities?: string[], - auth?: boolean - ): QueryConfig | ActionConfig { - return { - fn: { - import: name, - from: `@src/${importPath}`, - }, - entities: entities, - auth: auth || false, - }; - } -} diff --git a/packages/swarm-wasp/src/wasp-config/index.ts b/packages/swarm-wasp/src/wasp-config/index.ts deleted file mode 100644 index 284d3459..00000000 --- a/packages/swarm-wasp/src/wasp-config/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Export the main App class -export { App } from './app'; - -// Re-export wasp-config types for convenience -export type { - ActionConfig, - ApiConfig, - ApiNamespaceConfig, - AppConfig, - AuthConfig, - ClientConfig, - CrudConfig, - CrudOperationOptions, - DbConfig, - EmailSenderConfig, - JobConfig, - QueryConfig, - RouteConfig, -} from 'wasp-config'; diff --git a/packages/swarm-wasp/src/wasp-config/stubs/index.ts b/packages/swarm-wasp/src/wasp-config/stubs/index.ts index b36291eb..42518854 100644 --- a/packages/swarm-wasp/src/wasp-config/stubs/index.ts +++ b/packages/swarm-wasp/src/wasp-config/stubs/index.ts @@ -1,231 +1,351 @@ -// Stub wasp-config for development and testing -// This file provides the same interface as the real wasp-config package -// In production (Wasp project): will resolve to real wasp-config package - -export interface ActionConfig { - fn: { - import: string; - from: string; - }; - entities?: string[]; - auth: boolean; -} - -export interface ApiConfig { - fn: { - import: string; - from: string; - }; - middlewareConfigFn?: ExtImport; - entities?: string[]; - httpRoute: { - method: string; - route: string; - }; - auth: boolean; -} - -export interface ApiNamespaceConfig { +// Stub for `@wasp.sh/spec`, used for local typechecking and testing. +// +// It mirrors the public API of the real `@wasp.sh/spec` package (Wasp 0.24): +// the spec constructor functions and the types they operate on. In a real Wasp +// project this import resolves to the actual package; here it resolves to this +// stub via the `@wasp.sh/spec` path alias / linked devDependency. +// +// The runtime behaviour is intentionally minimal — the constructors return +// plainly-shaped spec objects. The real spec analysis is performed by Wasp, not +// by this package. + +/* -------------------------------------------------------------------------- */ +/* Shared helper types */ +/* -------------------------------------------------------------------------- */ + +// biome-ignore lint/suspicious/noExplicitAny: stub helper types mirror the real package +type AnyFunction = (...args: any[]) => any; +// biome-ignore lint/suspicious/noExplicitAny: stub helper types mirror the real package +type AnyObject = Record; +type ZodSchema = { parse: AnyFunction }; + +/** + * Seam for module augmentation (entities, etc.), mirrored from the real + * package. Simplified — the real package derives `EntityName` from this. + */ +// biome-ignore lint/suspicious/noEmptyInterface: augmentation seam mirrored from the real package +export interface Register {} + +/** Name of a Prisma entity. Simplified to `string` in the stub. */ +export type EntityName = string; + +/* -------------------------------------------------------------------------- */ +/* References */ +/* -------------------------------------------------------------------------- */ + +/** Named import reference (`import { X } from "./m" with { type: "ref" }`). */ +export interface NamedRefObjectDescriptor { + import: string; + alias?: string; + from: string; +} + +/** Default import reference (`import X from "./m" with { type: "ref" }`). */ +export interface DefaultRefObjectDescriptor { + importDefault: string; + from: string; +} + +export type RefObjectDescriptor = + | NamedRefObjectDescriptor + | DefaultRefObjectDescriptor; + +export type RefObject = RefObjectDescriptor & { kind: 'refObject' }; + +/** A reference to app code, or (when statically resolvable) the value itself. */ +export type Reference = RefObject | AppValue; + +/** Creates a fallback reference object for a value from your app's `src`. */ +export function ref(descriptor: RefObjectDescriptor): RefObject { + return { ...descriptor, kind: 'refObject' }; +} + +/* -------------------------------------------------------------------------- */ +/* Spec elements */ +/* -------------------------------------------------------------------------- */ + +interface BaseSpecElement { + kind: Kind; +} + +export interface Page extends BaseSpecElement<'page'> { + component: Reference; + authRequired?: boolean; +} + +export interface Route extends BaseSpecElement<'route'> { + name: string; path: string; - middlewareConfigFn: { - import: string; - from: string; - }; + page: Page; + prerender?: boolean; + lazy?: boolean; } -interface Wasp { - version: string; +export interface Query extends BaseSpecElement<'query'> { + fn: Reference; + entities?: EntityName[]; + auth?: boolean; } -export interface AppConfig { - title: string; - wasp: Wasp; - head?: string[]; +export interface Action extends BaseSpecElement<'action'> { + fn: Reference; + entities?: EntityName[]; + auth?: boolean; } -interface AuthMethods { - usernameAndPassword?: UsernameAndPasswordConfig; - discord?: ExternalAuthConfig; - google?: ExternalAuthConfig; - gitHub?: ExternalAuthConfig; - keycloak?: ExternalAuthConfig; - email?: EmailAuthConfig; +export type HttpMethod = 'ALL' | 'GET' | 'POST' | 'PUT' | 'DELETE'; + +export interface Api extends BaseSpecElement<'api'> { + method: HttpMethod; + path: string; + fn: Reference; + middlewareConfigFn?: Reference; + entities?: EntityName[]; + auth?: boolean; } -export interface AuthConfig { - userEntity: string; - methods: AuthMethods; - externalAuthEntity?: string; - onAuthFailedRedirectTo: string; - onAuthSucceededRedirectTo?: string; - onBeforeSignup?: ExtImport; - onAfterSignup?: ExtImport; - onAfterEmailVerified?: ExtImport; - onBeforeOAuthRedirect?: ExtImport; - onBeforeLogin?: ExtImport; - onAfterLogin?: ExtImport; +export interface ApiNamespace extends BaseSpecElement<'apiNamespace'> { + middlewareConfigFn: Reference; + path: string; } -interface UsernameAndPasswordConfig { - userSignupFields?: ExtImport; +export type JobExecutor = 'PgBoss'; + +export interface ExecutorOptions { + pgBoss: object; } -interface EmailAuthConfig { - userSignupFields?: ExtImport; - fromField: EmailFromField; - emailVerification: EmailVerificationConfig; - passwordReset: PasswordResetConfig; +export interface Schedule { + cron: string; + args?: object; + executorOptions?: ExecutorOptions; } -interface EmailVerificationConfig { - getEmailContentFn?: ExtImport; - clientRoute: string; +export interface Job extends BaseSpecElement<'job'> { + fn: Reference; + executor: JobExecutor; + schedule?: Schedule; + entities?: EntityName[]; + performExecutorOptions?: ExecutorOptions; +} + +export interface CrudOperationOptions { + isPublic?: boolean; + overrideFn?: Reference; +} + +export interface CrudOperations { + get?: CrudOperationOptions; + getAll?: CrudOperationOptions; + create?: CrudOperationOptions; + update?: CrudOperationOptions; + delete?: CrudOperationOptions; +} + +export interface Crud extends BaseSpecElement<'crud'> { + name: string; + entity: EntityName; + operations: CrudOperations; } -interface PasswordResetConfig { - getEmailContentFn?: ExtImport; +export type SpecElement = + | Page + | Route + | Query + | Action + | Api + | ApiNamespace + | Job + | Crud; + +/** A single spec element, or an (optionally nested) array of them. */ +export type Spec = SpecElement | Spec[]; + +/* -------------------------------------------------------------------------- */ +/* App-level configuration */ +/* -------------------------------------------------------------------------- */ + +export interface Wasp { + version: string; +} + +interface BaseAuthMethodConfig { + userSignupFields?: Reference; +} + +export interface UsernameAndPasswordConfig extends BaseAuthMethodConfig {} + +export interface SocialAuthConfig extends BaseAuthMethodConfig { + configFn?: Reference; +} + +export interface EmailFlowConfig { + getEmailContentFn?: Reference; clientRoute: string; } -interface ExternalAuthConfig { - configFn?: ExtImport; - userSignupFields?: ExtImport; +export interface EmailAuthConfig extends BaseAuthMethodConfig { + fromField: EmailFromField; + emailVerification: EmailFlowConfig; + passwordReset: EmailFlowConfig; } -type ExtImport = - | { - import: string; - from: AppSpecExtImport['path']; - } - | { - importDefault: string; - from: AppSpecExtImport['path']; - }; +type SocialAuthMethodName = + | 'discord' + | 'google' + | 'gitHub' + | 'keycloak' + | 'microsoft' + | 'slack'; + +export interface LocalAuthMethods { + usernameAndPassword: UsernameAndPasswordConfig; + email: EmailAuthConfig; +} + +export type ExternalAuthMethods = Partial< + Record +>; + +// Simplified from the real package's `RequireOneOrNone`. +export type AuthMethods = Partial & ExternalAuthMethods; -interface EmailFromField { +interface AuthHooks { + onBeforeSignup?: Reference; + onAfterSignup?: Reference; + onAfterEmailVerified?: Reference; + onBeforeOAuthRedirect?: Reference; + onBeforeLogin?: Reference; + onAfterLogin?: Reference; +} + +export interface Auth extends AuthHooks { + userEntity: EntityName; + methods: AuthMethods; + onAuthFailedRedirectTo: string; + onAuthSucceededRedirectTo?: string; +} + +export interface Server { + setupFn?: Reference; + middlewareConfigFn?: Reference; + envValidationSchema?: Reference; +} + +export interface Client { + rootComponent?: Reference; + setupFn?: Reference; + baseDir?: `/${string}`; + envValidationSchema?: Reference; +} + +export interface Db { + seeds?: Reference[]; + prismaSetupFn?: Reference; +} + +export type EmailSenderProviderName = 'SMTP' | 'SendGrid' | 'Mailgun' | 'Dummy'; + +export interface EmailFromField { name?: string; email: string; } -type ExtImportKind = 'named' | 'default'; -interface AppSpecExtImport { - kind: ExtImportKind; +export interface EmailSender { + provider: EmailSenderProviderName; + defaultFrom?: EmailFromField; +} + +export interface WebSocket { + fn: Reference; + autoConnect?: boolean; +} + +export interface App { name: string; - path: `@src/${string}`; + wasp: Wasp; + title: string; + head?: string[]; + auth?: Auth; + server?: Server; + client?: Client; + db?: Db; + emailSender?: EmailSender; + webSocket?: WebSocket; + spec: Spec; } -export interface ClientConfig { - rootComponent: string; +/* -------------------------------------------------------------------------- */ +/* Spec constructors */ +/* -------------------------------------------------------------------------- */ + +/** Creates a Wasp {@link App}. Export the result as the default export. */ +export function app(config: Omit): App { + return config as App; } -export interface CrudConfig { - entity: string; - operations: { - getAll?: CrudOperationOptions; - get?: CrudOperationOptions; - create?: CrudOperationOptions; - update?: CrudOperationOptions; - delete?: CrudOperationOptions; - }; +/** Creates a {@link Page} definition. */ +export function page( + component: Page['component'], + config?: Omit +): Page { + return { kind: 'page', component, ...config }; } -export interface CrudOperationOptions { - entities?: string[]; +/** Creates a {@link Route} definition. */ +export function route( + name: Route['name'], + path: Route['path'], + page: Route['page'], + config?: Omit +): Route { + return { kind: 'route', name, path, page, ...config }; } -export interface DbConfig { - system: string; +/** Creates a {@link Query} definition. */ +export function query( + fn: Query['fn'], + config?: Omit +): Query { + return { kind: 'query', fn, ...config }; } -export interface EmailSenderConfig { - // Add email sender config properties as needed - [key: string]: unknown; +/** Creates an {@link Action} definition. */ +export function action( + fn: Action['fn'], + config?: Omit +): Action { + return { kind: 'action', fn, ...config }; } -export interface JobConfig { - executor: string; - perform: { - fn: { - import: string; - from: string; - }; - }; - entities?: string[]; - schedule?: { - cron: string; - args: Record; - }; +/** Creates an {@link Api} endpoint definition. */ +export function api( + method: Api['method'], + path: Api['path'], + fn: Api['fn'], + config?: Omit +): Api { + return { kind: 'api', method, path, fn, ...config }; } -export interface QueryConfig { - fn: { - import: string; - from: string; - }; - entities?: string[]; - auth: boolean; +/** Creates an {@link ApiNamespace} definition. */ +export function apiNamespace( + path: ApiNamespace['path'], + config: Omit +): ApiNamespace { + return { kind: 'apiNamespace', path, ...config }; } -export interface RouteConfig { - path: string; - to: { - authRequired: boolean; - component: { - import: string; - from: string; - }; - }; +/** Creates a {@link Job} definition. */ +export function job(fn: Job['fn'], config: Omit): Job { + return { kind: 'job', fn, ...config }; } -export class App { - name: string; - config: AppConfig; - - constructor(name: string, config: AppConfig) { - this.name = name; - this.config = config; - } - - auth(_authConfig: AuthConfig): this { - return this; - } - client(_clientConfig: ClientConfig): this { - return this; - } - db(_dbConfig: DbConfig): this { - return this; - } - emailSender(_emailSenderConfig: EmailSenderConfig): this { - return this; - } - job(_name: string, _jobConfig: JobConfig): this { - return this; - } - query(_name: string, _queryConfig: QueryConfig): this { - return this; - } - route(_name: string, _routeConfig: RouteConfig): this { - return this; - } - api(_name: string, _apiConfig: ApiConfig): this { - return this; - } - apiNamespace(_name: string, _apiNamespaceConfig: ApiNamespaceConfig): this { - return this; - } - crud(_name: string, _crudConfig: CrudConfig): this { - return this; - } - action(_name: string, _actionConfig: ActionConfig): this { - return this; - } - page( - _componentName: string, - options: { - authRequired: boolean; - component: { import: string; from: string }; - } - ) { - return { authRequired: options.authRequired, component: options.component }; - } +/** Creates a {@link Crud} definition. */ +export function crud( + name: Crud['name'], + entity: Crud['entity'], + operations: Crud['operations'] +): Crud { + return { kind: 'crud', name, entity, operations }; } diff --git a/packages/swarm-wasp/src/wasp-config/stubs/package.json b/packages/swarm-wasp/src/wasp-config/stubs/package.json index 0e109c78..0b114bfd 100644 --- a/packages/swarm-wasp/src/wasp-config/stubs/package.json +++ b/packages/swarm-wasp/src/wasp-config/stubs/package.json @@ -1,5 +1,5 @@ { - "name": "wasp-config", + "name": "@wasp.sh/spec", "version": "0.0.0", "type": "module", "main": "index.js", diff --git a/packages/swarm-wasp/tests/action-generation.test.ts b/packages/swarm-wasp/tests/action-generation.test.ts index fa3a771f..f47a5c61 100644 --- a/packages/swarm-wasp/tests/action-generation.test.ts +++ b/packages/swarm-wasp/tests/action-generation.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ActionGenerator, FeatureGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as actionSchema } from '../src/generators/action/schema'; import { schema as featureSchema } from '../src/generators/feature/schema'; -import { realFileSystem } from '../src/common'; import { assertImportsPresent, countOccurrences, @@ -27,9 +27,13 @@ describe('Action Generator Integration Tests', () => { }); it('should generate create action with proper types and imports', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const actionGen = await createTestGenerator(ActionGenerator, actionSchema, { fileSystem: realFileSystem, }); @@ -58,9 +62,13 @@ describe('Action Generator Integration Tests', () => { }); it('should not duplicate action in config without force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const actionGen = await createTestGenerator(ActionGenerator, actionSchema, { fileSystem: realFileSystem, }); @@ -95,9 +103,13 @@ describe('Action Generator Integration Tests', () => { }); it('should replace action definition with force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const actionGen = await createTestGenerator(ActionGenerator, actionSchema, { fileSystem: realFileSystem, }); @@ -127,6 +139,6 @@ describe('Action Generator Integration Tests', () => { const contentAfter = readGeneratedFile(projectPaths.root, configPath); expect(contentAfter).toContain('auth: true'); - expect(countOccurrences(contentAfter, 'addAction')).toBeLessThanOrEqual(1); + expect(countOccurrences(contentAfter, 'action\\(')).toBeLessThanOrEqual(1); }); }); diff --git a/packages/swarm-wasp/tests/api-generation.test.ts b/packages/swarm-wasp/tests/api-generation.test.ts index 88344060..a6057a12 100644 --- a/packages/swarm-wasp/tests/api-generation.test.ts +++ b/packages/swarm-wasp/tests/api-generation.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ApiGenerator, FeatureGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as apiSchema } from '../src/generators/api/schema'; import { schema as featureSchema } from '../src/generators/feature/schema'; -import { realFileSystem } from '../src/common'; import { assertImportsPresent, countOccurrences, @@ -27,9 +27,13 @@ describe('API Generator Integration Tests', () => { }); it('should generate API endpoint with proper structure', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { fileSystem: realFileSystem, }); @@ -56,9 +60,13 @@ describe('API Generator Integration Tests', () => { }); it('should generate authenticated API endpoint', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { fileSystem: realFileSystem, }); @@ -83,9 +91,13 @@ describe('API Generator Integration Tests', () => { }); it('should generate API config with correct structure', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { fileSystem: realFileSystem, }); @@ -104,16 +116,20 @@ describe('API Generator Integration Tests', () => { const configPath = 'src/features/posts/feature.wasp.ts'; const content = readGeneratedFile(projectPaths.root, configPath); - expect(content).toContain('addApi'); + expect(content).toContain('api('); expect(content).toContain('postsApi'); - expect(content).toContain('route: "/api/posts"'); - expect(content).toContain('method: "GET"'); + expect(content).toContain('"/api/posts"'); + expect(content).toContain('"GET"'); }); it('should not duplicate API in config without force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { fileSystem: realFileSystem, }); diff --git a/packages/swarm-wasp/tests/api-namespace-generation.test.ts b/packages/swarm-wasp/tests/api-namespace-generation.test.ts index 20e05d32..ece19d5e 100644 --- a/packages/swarm-wasp/tests/api-namespace-generation.test.ts +++ b/packages/swarm-wasp/tests/api-namespace-generation.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ApiNamespaceGenerator, FeatureGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as apiNamespaceSchema } from '../src/generators/api-namespace/schema'; import { schema as featureSchema } from '../src/generators/feature/schema'; -import { realFileSystem } from '../src/common'; import { countOccurrences, createTestGenerator, @@ -26,12 +26,20 @@ describe('API Namespace Generator Integration Tests', () => { }); it('should generate API namespace middleware', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); - const apiNamespaceGen = await createTestGenerator(ApiNamespaceGenerator, apiNamespaceSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); + const apiNamespaceGen = await createTestGenerator( + ApiNamespaceGenerator, + apiNamespaceSchema, + { + fileSystem: realFileSystem, + } + ); await featureGen.generate({ target: 'posts' }); await apiNamespaceGen.generate({ @@ -41,7 +49,8 @@ describe('API Namespace Generator Integration Tests', () => { force: false, }); - const middlewarePath = 'src/features/posts/server/apis/middleware/postsApi.ts'; + const middlewarePath = + 'src/features/posts/server/apis/middleware/postsApi.ts'; const content = readGeneratedFile(projectPaths.root, middlewarePath); expect(content).toContain('export const postsApi'); @@ -49,12 +58,20 @@ describe('API Namespace Generator Integration Tests', () => { }); it('should generate API namespace config', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); - const apiNamespaceGen = await createTestGenerator(ApiNamespaceGenerator, apiNamespaceSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); + const apiNamespaceGen = await createTestGenerator( + ApiNamespaceGenerator, + apiNamespaceSchema, + { + fileSystem: realFileSystem, + } + ); await featureGen.generate({ target: 'posts' }); await apiNamespaceGen.generate({ @@ -67,19 +84,27 @@ describe('API Namespace Generator Integration Tests', () => { const configPath = 'src/features/posts/feature.wasp.ts'; const content = readGeneratedFile(projectPaths.root, configPath); - expect(content).toContain('addApiNamespace'); + expect(content).toContain('apiNamespace('); expect(content).toContain('postsApi'); - expect(content).toContain('addApiNamespace'); - expect(content).toContain('path: "/api/posts"'); + expect(content).toContain('apiNamespace('); + expect(content).toContain('"/api/posts"'); }); it('should not duplicate API namespace in config without force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); - const apiNamespaceGen = await createTestGenerator(ApiNamespaceGenerator, apiNamespaceSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); + const apiNamespaceGen = await createTestGenerator( + ApiNamespaceGenerator, + apiNamespaceSchema, + { + fileSystem: realFileSystem, + } + ); await featureGen.generate({ target: 'posts' }); await apiNamespaceGen.generate({ diff --git a/packages/swarm-wasp/tests/config-file-management.test.ts b/packages/swarm-wasp/tests/config-file-management.test.ts index 914ea122..8df41207 100644 --- a/packages/swarm-wasp/tests/config-file-management.test.ts +++ b/packages/swarm-wasp/tests/config-file-management.test.ts @@ -7,13 +7,13 @@ import { QueryGenerator, RouteGenerator, } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as actionSchema } from '../src/generators/action/schema'; import { schema as apiSchema } from '../src/generators/api/schema'; import { schema as featureSchema } from '../src/generators/feature/schema'; import { schema as jobSchema } from '../src/generators/job/schema'; import { schema as querySchema } from '../src/generators/query/schema'; import { schema as routeSchema } from '../src/generators/route/schema'; -import { realFileSystem } from '../src/common'; import { countOccurrences, createTestGenerator, @@ -100,11 +100,11 @@ describe('Configuration File Management Tests', () => { const content = readGeneratedFile(projectPaths.root, configPath); // The actual generated format doesn't have group headers, just comments - expect(content).toContain('addRoute'); - expect(content).toContain('addQuery'); - expect(content).toContain('addAction'); - expect(content).toContain('addApi'); - expect(content).toContain('addJob'); + expect(content).toContain('route('); + expect(content).toContain('query('); + expect(content).toContain('action('); + expect(content).toContain('api('); + expect(content).toContain('job('); }); it('should include group headers for each definition type', async () => { @@ -156,7 +156,7 @@ describe('Configuration File Management Tests', () => { const content = readGeneratedFile(projectPaths.root, configPath); // The generated config ends with a closing brace, not semicolon - expect(content.trim()).toMatch(/}\s*$/); + expect(content.trim()).toMatch(/];\s*$/); }); it('should preserve proper structure after multiple additions', async () => { @@ -191,10 +191,10 @@ describe('Configuration File Management Tests', () => { const content = readGeneratedFile(projectPaths.root, configPath); // The generated config should end with a closing brace - expect(content.trim()).toMatch(/}\s*$/); + expect(content.trim()).toMatch(/];\s*$/); // Should have multiple action definitions - expect(countOccurrences(content, 'addAction')).toBe(3); + expect(countOccurrences(content, 'action\\(')).toBe(3); }); it('should sort definitions alphabetically within groups', async () => { diff --git a/packages/swarm-wasp/tests/cross-generator.test.ts b/packages/swarm-wasp/tests/cross-generator.test.ts index 234d86ef..8153598c 100644 --- a/packages/swarm-wasp/tests/cross-generator.test.ts +++ b/packages/swarm-wasp/tests/cross-generator.test.ts @@ -8,6 +8,7 @@ import { QueryGenerator, RouteGenerator, } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as actionSchema } from '../src/generators/action/schema'; import { schema as apiSchema } from '../src/generators/api/schema'; import { schema as crudSchema } from '../src/generators/crud/schema'; @@ -15,7 +16,6 @@ import { schema as featureSchema } from '../src/generators/feature/schema'; import { schema as jobSchema } from '../src/generators/job/schema'; import { schema as querySchema } from '../src/generators/query/schema'; import { schema as routeSchema } from '../src/generators/route/schema'; -import { realFileSystem } from '../src/common'; import { assertConfigGroupOrder, createTestGenerator, @@ -80,16 +80,16 @@ describe('Cross-Generator Integration Tests', () => { const configPath = 'src/features/posts/feature.wasp.ts'; const content = readGeneratedFile(projectPaths.root, configPath); - expect(content).toContain('addCrud'); - expect(content).toContain('addAction'); - expect(content).toContain('addQuery'); + expect(content).toContain('crud('); + expect(content).toContain('action('); + expect(content).toContain('query('); expect(content).toContain('createPost'); expect(content).toContain('getPost'); // The actual generated format doesn't have group headers, just comments - expect(content).toContain('addQuery'); - expect(content).toContain('addAction'); - expect(content).toContain('addCrud'); + expect(content).toContain('query('); + expect(content).toContain('action('); + expect(content).toContain('crud('); }); it('should generate complete feature with all generator types', async () => { @@ -159,7 +159,7 @@ describe('Cross-Generator Integration Tests', () => { 'Jobs', ]); - expect(content.trim()).toMatch(/}\s*$/); + expect(content.trim()).toMatch(/];\s*$/); expect(content).toContain('postsPage'); expect(content).toContain('getAllPosts'); @@ -200,13 +200,13 @@ describe('Cross-Generator Integration Tests', () => { const postsContent = readGeneratedFile(projectPaths.root, postsConfig); const usersContent = readGeneratedFile(projectPaths.root, usersConfig); - expect(postsContent).toContain('addAction'); + expect(postsContent).toContain('action('); expect(postsContent).toContain('createPost'); - expect(usersContent).toContain('addCrud'); + expect(usersContent).toContain('crud('); expect(usersContent).toContain('User'); - expect(postsContent.trim()).toMatch(/}\s*$/); - expect(usersContent.trim()).toMatch(/}\s*$/); + expect(postsContent.trim()).toMatch(/];\s*$/); + expect(usersContent.trim()).toMatch(/];\s*$/); }); }); diff --git a/packages/swarm-wasp/tests/crud-generation.test.ts b/packages/swarm-wasp/tests/crud-generation.test.ts index 6e32ca13..21e36fec 100644 --- a/packages/swarm-wasp/tests/crud-generation.test.ts +++ b/packages/swarm-wasp/tests/crud-generation.test.ts @@ -107,10 +107,8 @@ describe('CRUD Generator Integration Tests', () => { ); expect(realFileSystem.existsSync(crudPath)).toBe(false); - expect(content).toContain('addCrud'); - expect(content).toContain('Post'); - expect(content).toContain('posts'); - expect(content).toContain('entity: "Post"'); + // The crud is declared with a PascalCase name matching its generated type. + expect(content).toContain('crud("Posts", "Post"'); }); it('should not duplicate CRUD in config without force flag', async () => { @@ -136,19 +134,21 @@ describe('CRUD Generator Integration Tests', () => { const configPath = 'src/features/posts/feature.wasp.ts'; const contentBefore = readGeneratedFile(projectPaths.root, configPath); - const occurrencesBefore = countOccurrences(contentBefore, 'addCrud'); - - // The CRUD generator should replace the existing definition, not duplicate it - await crudGen.generate({ - dataType: 'Post', - feature: 'posts', - name: 'posts', - public: ['create', 'get'], - force: false, - }); + const occurrencesBefore = countOccurrences(contentBefore, 'crud\\('); + + // Without --force, re-adding an existing crud is rejected (not duplicated). + await expect( + crudGen.generate({ + dataType: 'Post', + feature: 'posts', + name: 'posts', + public: ['create', 'get'], + force: false, + }) + ).rejects.toThrow(); const contentAfter = readGeneratedFile(projectPaths.root, configPath); - const occurrencesAfter = countOccurrences(contentAfter, 'addCrud'); + const occurrencesAfter = countOccurrences(contentAfter, 'crud\\('); const crudPath = path.join( projectPaths.root, 'src/features/posts/server/cruds/posts.ts' diff --git a/packages/swarm-wasp/tests/job-generation.test.ts b/packages/swarm-wasp/tests/job-generation.test.ts index 958e314e..1a5770aa 100644 --- a/packages/swarm-wasp/tests/job-generation.test.ts +++ b/packages/swarm-wasp/tests/job-generation.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { FeatureGenerator, JobGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as featureSchema } from '../src/generators/feature/schema'; import { schema as jobSchema } from '../src/generators/job/schema'; -import { realFileSystem } from '../src/common'; import { countOccurrences, createTestGenerator, @@ -26,9 +26,13 @@ describe('Job Generator Integration Tests', () => { }); it('should generate job with proper structure', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const jobGen = await createTestGenerator(JobGenerator, jobSchema, { fileSystem: realFileSystem, }); @@ -49,9 +53,13 @@ describe('Job Generator Integration Tests', () => { }); it('should generate job config with cron schedule', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const jobGen = await createTestGenerator(JobGenerator, jobSchema, { fileSystem: realFileSystem, }); @@ -67,15 +75,19 @@ describe('Job Generator Integration Tests', () => { const configPath = 'src/features/posts/feature.wasp.ts'; const content = readGeneratedFile(projectPaths.root, configPath); - expect(content).toContain('addJob'); + expect(content).toContain('job('); expect(content).toContain('cleanupPosts'); expect(content).toContain('cron: "0 0 * * *"'); }); it('should generate job with custom args', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const jobGen = await createTestGenerator(JobGenerator, jobSchema, { fileSystem: realFileSystem, }); @@ -96,9 +108,13 @@ describe('Job Generator Integration Tests', () => { }); it('should not duplicate job in config without force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const jobGen = await createTestGenerator(JobGenerator, jobSchema, { fileSystem: realFileSystem, }); diff --git a/packages/swarm-wasp/tests/query-generation.test.ts b/packages/swarm-wasp/tests/query-generation.test.ts index a79346f4..f4aa4e44 100644 --- a/packages/swarm-wasp/tests/query-generation.test.ts +++ b/packages/swarm-wasp/tests/query-generation.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { FeatureGenerator, QueryGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as featureSchema } from '../src/generators/feature/schema'; import { schema as querySchema } from '../src/generators/query/schema'; -import { realFileSystem } from '../src/common'; import { assertImportsPresent, countOccurrences, @@ -27,9 +27,13 @@ describe('Query Generator Integration Tests', () => { }); it('should generate get query with proper types and imports', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const queryGen = await createTestGenerator(QueryGenerator, querySchema, { fileSystem: realFileSystem, }); @@ -56,9 +60,13 @@ describe('Query Generator Integration Tests', () => { }); it('should generate getAll query with proper filtering', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const queryGen = await createTestGenerator(QueryGenerator, querySchema, { fileSystem: realFileSystem, }); @@ -80,9 +88,13 @@ describe('Query Generator Integration Tests', () => { }); it('should not duplicate query in config without force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const queryGen = await createTestGenerator(QueryGenerator, querySchema, { fileSystem: realFileSystem, }); diff --git a/packages/swarm-wasp/tests/route-generation.test.ts b/packages/swarm-wasp/tests/route-generation.test.ts index 19af28ea..9d03cccb 100644 --- a/packages/swarm-wasp/tests/route-generation.test.ts +++ b/packages/swarm-wasp/tests/route-generation.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { FeatureGenerator, RouteGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as featureSchema } from '../src/generators/feature/schema'; import { schema as routeSchema } from '../src/generators/route/schema'; -import { realFileSystem } from '../src/common'; import { countOccurrences, createTestGenerator, @@ -26,9 +26,13 @@ describe('Route Generator Integration Tests', () => { }); it('should generate route page with proper React component', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const routeGen = await createTestGenerator(RouteGenerator, routeSchema, { fileSystem: realFileSystem, }); @@ -51,9 +55,13 @@ describe('Route Generator Integration Tests', () => { }); it('should generate authenticated route', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const routeGen = await createTestGenerator(RouteGenerator, routeSchema, { fileSystem: realFileSystem, }); @@ -74,9 +82,13 @@ describe('Route Generator Integration Tests', () => { }); it('should generate route config with correct structure', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const routeGen = await createTestGenerator(RouteGenerator, routeSchema, { fileSystem: realFileSystem, }); @@ -92,15 +104,19 @@ describe('Route Generator Integration Tests', () => { const configPath = 'src/features/posts/feature.wasp.ts'; const content = readGeneratedFile(projectPaths.root, configPath); - expect(content).toContain('addRoute'); - expect(content).toContain('path: "/posts"'); + expect(content).toContain('route('); + expect(content).toContain('"/posts"'); expect(content).toContain('posts'); }); it('should not duplicate route in config without force flag', async () => { - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const routeGen = await createTestGenerator(RouteGenerator, routeSchema, { fileSystem: realFileSystem, }); diff --git a/packages/swarm-wasp/tests/template-override.test.ts b/packages/swarm-wasp/tests/template-override.test.ts index a02c2992..10fe8854 100644 --- a/packages/swarm-wasp/tests/template-override.test.ts +++ b/packages/swarm-wasp/tests/template-override.test.ts @@ -1,14 +1,14 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { DEFAULT_CONFIG_FILE, DEFAULT_CUSTOM_TEMPLATES_DIR, } from '@ingenyus/swarm'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ApiGenerator, FeatureGenerator } from '../src'; +import { realFileSystem } from '../src/common'; import { schema as apiSchema } from '../src/generators/api/schema'; import { schema as featureSchema } from '../src/generators/feature/schema'; -import { realFileSystem } from '../src/common'; import { createTestGenerator, createTestWaspProject, @@ -16,6 +16,9 @@ import { type TestProjectPaths, } from './utils'; +// Only the code-file templates (e.g. `api.eta`) are user-overridable. Config +// declarations are generated in code (not templated), so a custom config +// template has no effect. describe('Template Override Integration Tests', () => { let projectPaths: TestProjectPaths; let originalCwd: string; @@ -30,73 +33,25 @@ describe('Template Override Integration Tests', () => { process.chdir(originalCwd); }); - it('should use custom template when it exists', async () => { - const customTemplateDir = DEFAULT_CUSTOM_TEMPLATES_DIR; - const customApiTemplatePath = path.join( - projectPaths.root, - customTemplateDir, - 'wasp', - 'api', - 'api.eta' - ); - const customConfigTemplatePath = path.join( - projectPaths.root, - customTemplateDir, - 'wasp', - 'api', - 'config', - 'api.eta' - ); - - fs.mkdirSync(path.dirname(customApiTemplatePath), { recursive: true }); - fs.mkdirSync(path.dirname(customConfigTemplatePath), { recursive: true }); - fs.writeFileSync( - customApiTemplatePath, - `// CUSTOM API TEMPLATE -<%=imports%> - -export const <%=apiName%>: <%=apiType%> = async (req, res, context) => { -<%=authCheck%><%=methodCheck%> // Custom implementation - res.json({ message: 'Custom API: <%=apiName%>' }); -}; -` - ); - fs.writeFileSync( - customConfigTemplatePath, - ` .addApi(feature, "<%=apiName%>", { - method: "<%=method%>", - route: "<%=route%>", -<%- if (entities) { %> - entities: [<%=entities%>], -<%- } %> - auth: <%=auth%>, -<%- if (customMiddleware === 'true') { %> - customMiddleware: true, -<%- } %> - // Custom config template feature - customFeature: true, - }) -` - ); - + function writeSwarmConfig(templateDirectory?: string): void { const swarmConfig = { - templateDirectory: customTemplateDir, - plugins: { - wasp: { - enabled: true, - plugin: 'wasp', - }, - }, + ...(templateDirectory ? { templateDirectory } : {}), + plugins: { wasp: { enabled: true, plugin: 'wasp' } }, }; - fs.writeFileSync( path.join(projectPaths.root, DEFAULT_CONFIG_FILE), JSON.stringify(swarmConfig, null, 2) ); + } - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); + async function generateApi(name: string, overrides = {}): Promise { + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } + ); const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { fileSystem: realFileSystem, }); @@ -104,203 +59,103 @@ export const <%=apiName%>: <%=apiType%> = async (req, res, context) => { await featureGen.generate({ target: 'posts' }); await apiGen.generate({ feature: 'posts', - name: 'customApi', + name, method: 'GET', - path: '/api/posts/custom', + path: `/api/posts/${name}`, entities: ['Post'], auth: false, force: true, + ...overrides, }); + } + + it('should use a custom code template when it exists', async () => { + const customApiTemplatePath = path.join( + projectPaths.root, + DEFAULT_CUSTOM_TEMPLATES_DIR, + 'wasp', + 'api', + 'api.eta' + ); + fs.mkdirSync(path.dirname(customApiTemplatePath), { recursive: true }); + fs.writeFileSync( + customApiTemplatePath, + `// CUSTOM API TEMPLATE +<%=imports%> + +export const <%=apiName%>: <%=apiType%> = async (req, res, context) => { +<%=authCheck%><%=methodCheck%> // Custom implementation + res.json({ message: 'Custom API: <%=apiName%>' }); +}; +` + ); + writeSwarmConfig(DEFAULT_CUSTOM_TEMPLATES_DIR); + + await generateApi('customApi'); const apiContent = readGeneratedFile( projectPaths.root, 'src/features/posts/server/apis/customApi.ts' ); - expect(apiContent).toContain('// CUSTOM API TEMPLATE'); expect(apiContent).toContain('Custom API: customApi'); - expect(apiContent).toContain('Custom implementation'); + // The config declaration is always code-generated, regardless of templates. const configContent = readGeneratedFile( projectPaths.root, 'src/features/posts/feature.wasp.ts' ); - - expect(configContent).toContain('customFeature: true'); + expect(configContent).toContain('api('); expect(configContent).toContain('customApi'); }); - it('should fall back to built-in template when custom does not exist', async () => { - const swarmConfig = { - templateDirectory: DEFAULT_CUSTOM_TEMPLATES_DIR, - plugins: { - wasp: { - enabled: true, - plugin: 'wasp', - }, - }, - }; - - fs.writeFileSync( - path.join(projectPaths.root, DEFAULT_CONFIG_FILE), - JSON.stringify(swarmConfig, null, 2) - ); - - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); - const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { - fileSystem: realFileSystem, - }); + it('should fall back to the built-in template when no custom exists', async () => { + writeSwarmConfig(DEFAULT_CUSTOM_TEMPLATES_DIR); - await featureGen.generate({ target: 'posts' }); - await apiGen.generate({ - feature: 'posts', - name: 'builtInApi', - method: 'POST', - path: '/api/posts', - entities: ['Post'], - auth: true, - force: true, - }); + await generateApi('builtInApi', { method: 'POST', auth: true }); const apiContent = readGeneratedFile( projectPaths.root, 'src/features/posts/server/apis/builtInApi.ts' ); - expect(apiContent).not.toContain('// CUSTOM API TEMPLATE'); expect(apiContent).toContain('export const builtInApi: BuiltInApi = async'); expect(apiContent).toContain('import { HttpError } from "wasp/server"'); - - const configContent = readGeneratedFile( - projectPaths.root, - 'src/features/posts/feature.wasp.ts' - ); - - expect(configContent).not.toContain('customFeature: true'); - expect(configContent).toContain('builtInApi'); - }); - - it('should handle nested template paths correctly', async () => { - const customTemplateDir = DEFAULT_CUSTOM_TEMPLATES_DIR; - const customConfigTemplatePath = path.join( - projectPaths.root, - customTemplateDir, - 'wasp', - 'api', - 'config', - 'api.eta' - ); - - fs.mkdirSync(path.dirname(customConfigTemplatePath), { recursive: true }); - fs.writeFileSync( - customConfigTemplatePath, - ` .addApi(feature, "<%=apiName%>", { - method: "<%=method%>", - route: "<%=route%>", -<%- if (entities) { %> - entities: [<%=entities%>], -<%- } %> - auth: <%=auth%>, -<%- if (customMiddleware === 'true') { %> - customMiddleware: true, -<%- } %> - // Custom nested template feature - customFeature: true, - }) -` - ); - - const swarmConfig = { - templateDirectory: customTemplateDir, - plugins: { - wasp: { - enabled: true, - plugin: 'wasp', - }, - }, - }; - - fs.writeFileSync( - path.join(projectPaths.root, DEFAULT_CONFIG_FILE), - JSON.stringify(swarmConfig, null, 2) - ); - - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); - const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { - fileSystem: realFileSystem, - }); - - await featureGen.generate({ target: 'posts' }); - await apiGen.generate({ - feature: 'posts', - name: 'nestedApi', - method: 'PUT', - path: '/api/posts/nested', - entities: ['Post'], - auth: false, - force: true, - }); - - const configContent = readGeneratedFile( - projectPaths.root, - 'src/features/posts/feature.wasp.ts' - ); - - expect(configContent).toContain('customFeature: true'); - expect(configContent).toContain('nestedApi'); }); - it('should validate custom template syntax and throw error for invalid templates', async () => { - const customTemplateDir = DEFAULT_CUSTOM_TEMPLATES_DIR; + it('should throw for a custom template with invalid syntax', async () => { const customApiTemplatePath = path.join( projectPaths.root, - customTemplateDir, + DEFAULT_CUSTOM_TEMPLATES_DIR, 'wasp', 'api', 'api.eta' ); - fs.mkdirSync(path.dirname(customApiTemplatePath), { recursive: true }); fs.writeFileSync( customApiTemplatePath, `// INVALID TEMPLATE <%=imports%> - export const <%=apiName%>: <%=apiType%> = async (req, res, context) => { -<%=authCheck%><%=methodCheck%> <% if (auth) { %> - // Missing closing tag - res.json({ message: 'Invalid template' }); + <% if (auth) { %> + // Missing closing tag }; ` ); + writeSwarmConfig(DEFAULT_CUSTOM_TEMPLATES_DIR); - const swarmConfig = { - templateDirectory: customTemplateDir, - plugins: { - wasp: { - enabled: true, - plugin: 'wasp', - }, - }, - }; - - fs.writeFileSync( - path.join(projectPaths.root, DEFAULT_CONFIG_FILE), - JSON.stringify(swarmConfig, null, 2) + const featureGen = await createTestGenerator( + FeatureGenerator, + featureSchema, + { + fileSystem: realFileSystem, + } ); - - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { fileSystem: realFileSystem, }); - await featureGen.generate({ target: 'posts' }); + await expect( apiGen.generate({ feature: 'posts', @@ -314,16 +169,14 @@ export const <%=apiName%>: <%=apiType%> = async (req, res, context) => { ).rejects.toThrow('Bad template syntax'); }); - it('should use default template directory when not specified in config', async () => { - const defaultTemplateDir = DEFAULT_CUSTOM_TEMPLATES_DIR; + it('should use the default template directory when not specified', async () => { const customApiTemplatePath = path.join( projectPaths.root, - defaultTemplateDir, + DEFAULT_CUSTOM_TEMPLATES_DIR, 'wasp', 'api', 'api.eta' ); - fs.mkdirSync(path.dirname(customApiTemplatePath), { recursive: true }); fs.writeFileSync( customApiTemplatePath, @@ -335,44 +188,14 @@ export const <%=apiName%>: <%=apiType%> = async (req, res, context) => { }; ` ); + writeSwarmConfig(); - const swarmConfig = { - plugins: { - wasp: { - enabled: true, - plugin: 'wasp', - }, - }, - }; - - fs.writeFileSync( - path.join(projectPaths.root, DEFAULT_CONFIG_FILE), - JSON.stringify(swarmConfig, null, 2) - ); - - const featureGen = await createTestGenerator(FeatureGenerator, featureSchema, { - fileSystem: realFileSystem, - }); - const apiGen = await createTestGenerator(ApiGenerator, apiSchema, { - fileSystem: realFileSystem, - }); - - await featureGen.generate({ target: 'posts' }); - await apiGen.generate({ - feature: 'posts', - name: 'defaultApi', - method: 'DELETE', - path: '/api/posts/default', - entities: ['Post'], - auth: false, - force: true, - }); + await generateApi('defaultApi', { method: 'DELETE' }); const apiContent = readGeneratedFile( projectPaths.root, 'src/features/posts/server/apis/defaultApi.ts' ); - expect(apiContent).toContain('// DEFAULT LOCATION TEMPLATE'); expect(apiContent).toContain('Default location template: defaultApi'); }); diff --git a/packages/swarm-wasp/tests/utils/assertions.ts b/packages/swarm-wasp/tests/utils/assertions.ts index b35df95c..f747ab1d 100644 --- a/packages/swarm-wasp/tests/utils/assertions.ts +++ b/packages/swarm-wasp/tests/utils/assertions.ts @@ -1,7 +1,10 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; -export function readGeneratedFile(projectRoot: string, relativePath: string): string { +export function readGeneratedFile( + projectRoot: string, + relativePath: string +): string { return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8'); } @@ -17,7 +20,7 @@ export function assertConfigGroupOrder( content: string, expectedOrder: string[] ): void { - const positions = expectedOrder.map(group => { + const positions = expectedOrder.map((group) => { const match = content.match(new RegExp(`//\\s*${group}`, 'i')); return match ? match.index! : -1; }); diff --git a/packages/swarm-wasp/tests/utils/global-setup.ts b/packages/swarm-wasp/tests/utils/global-setup.ts index fd8cf85a..d59b6b66 100644 --- a/packages/swarm-wasp/tests/utils/global-setup.ts +++ b/packages/swarm-wasp/tests/utils/global-setup.ts @@ -11,7 +11,7 @@ export default async function globalSetup() { if (fs.existsSync(outDir)) { try { fs.rmSync(outDir, { recursive: true, force: true }); - } catch (error) { + } catch (_error) { // Ignore errors during cleanup } } diff --git a/packages/swarm-wasp/tests/utils/index.ts b/packages/swarm-wasp/tests/utils/index.ts index ee3dbd6d..ccd97b70 100644 --- a/packages/swarm-wasp/tests/utils/index.ts +++ b/packages/swarm-wasp/tests/utils/index.ts @@ -1,4 +1,3 @@ export * from './assertions'; export * from './mocks'; export * from './wasp'; - diff --git a/packages/swarm-wasp/tests/utils/mocks.ts b/packages/swarm-wasp/tests/utils/mocks.ts index 095fc09a..05088ce7 100644 --- a/packages/swarm-wasp/tests/utils/mocks.ts +++ b/packages/swarm-wasp/tests/utils/mocks.ts @@ -1,6 +1,5 @@ import type { FileSystem, - Generator, GeneratorBase, GeneratorProvider, GeneratorServices, @@ -32,17 +31,6 @@ export function createMockFS(): FileSystem { } as FileSystem; } -export function createMockFeatureGen( - s: S -): Generator { - return { - name: 'feature', - description: 'Mock', - schema: s, - generate: vi.fn(), - }; -} - /** * Convenience helper for creating generators in tests with default mocks. * Automatically provides mock filesystem and logger, with optional overrides. @@ -62,7 +50,7 @@ export function createMockFeatureGen( */ export async function createTestGenerator>( ctor: new (services: GeneratorServices) => T, - schema: StandardSchemaV1, + _schema: StandardSchemaV1, overrides: Partial = {} ): Promise { const mockFS = createMockFS(); diff --git a/packages/swarm-wasp/tests/utils/wasp.ts b/packages/swarm-wasp/tests/utils/wasp.ts index b05f4cd5..3cecc74d 100644 --- a/packages/swarm-wasp/tests/utils/wasp.ts +++ b/packages/swarm-wasp/tests/utils/wasp.ts @@ -1,7 +1,7 @@ -import { DEFAULT_CONFIG_FILE } from '@ingenyus/swarm'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { DEFAULT_CONFIG_FILE } from '@ingenyus/swarm'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -32,19 +32,30 @@ export function createTestWaspProject(): TestProjectPaths { fs.writeFileSync(path.join(root, '.wasproot'), 'wasp'); + // Native Wasp 0.24 spec: `main.wasp.ts` lives at the project root and pulls in + // the generated features barrel. const mainWasp = path.join(root, 'main.wasp.ts'); fs.writeFileSync( mainWasp, - ` -import { app } from '@wasp/config'; + `import { app } from '@wasp.sh/spec'; +import { featureSpecs } from './src/features/index.wasp'; -export default app('TestApp', { - wasp: { version: '^0.18.0' }, +export default app({ + name: 'TestApp', title: 'Test Application', + wasp: { version: '^0.24.0' }, + spec: [featureSpecs], }); ` ); + // Empty features barrel so `main.wasp.ts` resolves before any feature exists; + // it is regenerated as features are added. + fs.writeFileSync( + path.join(features, 'index.wasp.ts'), + `import type { Spec } from '@wasp.sh/spec';\n\nexport const featureSpecs: Spec = [];\n` + ); + const schema = path.join(root, 'schema.prisma'); fs.writeFileSync( schema, @@ -100,9 +111,9 @@ model Comment { name: 'test-wasp-app', version: '0.0.1', private: true, + type: 'module', dependencies: { - '@wasp/entities': 'workspace:*', - wasp: '^0.18.0', + '@wasp.sh/spec': '^0.24.0', }, }, null, @@ -110,16 +121,55 @@ model Comment { ) ); + // Wasp 0.24 tsconfig split: the root config references a `src` project (app + // code, excludes *.wasp.ts) and a `wasp` project (the *.wasp.ts spec files). fs.writeFileSync( path.join(root, 'tsconfig.json'), JSON.stringify( { - extends: './tsconfig.wasp.json', + files: [], + references: [ + { path: './tsconfig.src.json' }, + { path: './tsconfig.wasp.json' }, + ], + }, + null, + 2 + ) + ); + + fs.writeFileSync( + path.join(root, 'tsconfig.src.json'), + JSON.stringify( + { + compilerOptions: { + module: 'esnext', + moduleResolution: 'bundler', + composite: true, + strict: true, + jsx: 'preserve', + skipLibCheck: true, + }, + include: ['src'], + exclude: ['**/*.wasp.ts'], + }, + null, + 2 + ) + ); + + fs.writeFileSync( + path.join(root, 'tsconfig.wasp.json'), + JSON.stringify( + { compilerOptions: { + module: 'esnext', + moduleResolution: 'bundler', strict: true, - esModuleInterop: true, skipLibCheck: true, + noEmit: true, }, + include: ['**/*.wasp.ts'], }, null, 2 diff --git a/packages/swarm-wasp/tsconfig.json b/packages/swarm-wasp/tsconfig.json index 38ee5e4a..86f62fae 100644 --- a/packages/swarm-wasp/tsconfig.json +++ b/packages/swarm-wasp/tsconfig.json @@ -8,7 +8,7 @@ "tsBuildInfoFile": "./dist/.tsbuildinfo", "noEmit": false, "paths": { - "wasp-config": ["./src/wasp-config/stubs"] + "@wasp.sh/spec": ["./src/wasp-config/stubs"] } }, "include": ["src/**/*", "tests/**/*"], diff --git a/packages/swarm/.prettierignore b/packages/swarm/.prettierignore deleted file mode 100644 index 0f9c450e..00000000 --- a/packages/swarm/.prettierignore +++ /dev/null @@ -1,9 +0,0 @@ -dist/ -node_modules/ -.git/ -*.log -*.tgz -.DS_Store -pnpm-lock.yaml -src/templates/ -CHANGELOG.md \ No newline at end of file diff --git a/packages/swarm/.prettierrc.json b/packages/swarm/.prettierrc.json deleted file mode 100644 index 8d282d5f..00000000 --- a/packages/swarm/.prettierrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "arrowParens": "always", - "endOfLine": "lf" -} \ No newline at end of file diff --git a/packages/swarm/eslint.config.js b/packages/swarm/eslint.config.js deleted file mode 100644 index f2a0681d..00000000 --- a/packages/swarm/eslint.config.js +++ /dev/null @@ -1,49 +0,0 @@ -import typescript from '@typescript-eslint/eslint-plugin'; -import typescriptParser from '@typescript-eslint/parser'; -import baseConfig from '../../eslint.config.js'; - -export default [ - ...baseConfig, - { - files: ['src/**/*.ts'], - languageOptions: { - parser: typescriptParser, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - project: './tsconfig.json', - }, - globals: { - __dirname: 'readonly', - process: 'readonly', - console: 'readonly', - Buffer: 'readonly', - require: 'readonly', - module: 'readonly', - BufferEncoding: 'readonly', - vi: 'readonly', - describe: 'readonly', - it: 'readonly', - test: 'readonly', - expect: 'readonly', - beforeEach: 'readonly', - afterEach: 'readonly', - beforeAll: 'readonly', - afterAll: 'readonly', - NodeJS: 'readonly', - global: 'readonly', - }, - }, - plugins: { - '@typescript-eslint': typescript, - }, - rules: { - // Disable base no-unused-vars in favor of @typescript-eslint/no-unused-vars - 'no-unused-vars': 'off', - // Package-specific overrides can go here - }, - }, - { - ignores: ['dist/', 'node_modules/', '*.js', 'src/templates/', 'tests/*.d.ts', 'tests/*.d.ts.map', 'tests/mcp/output/**/*'], - }, -]; diff --git a/packages/swarm/package.json b/packages/swarm/package.json index 2b7c6db2..7e610e77 100644 --- a/packages/swarm/package.json +++ b/packages/swarm/package.json @@ -21,10 +21,10 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "lint": "eslint src --ext .ts --fix", - "lint:check": "eslint src --ext .ts", - "format": "prettier --write --list-different src", - "format:check": "prettier --check src", + "lint": "biome check --write .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format .", "typecheck": "tsc --project tsconfig.typecheck.json", "validate": "pnpm run build && pnpm run typecheck && pnpm test && pnpm run lint && pnpm run format" }, @@ -66,13 +66,13 @@ }, "devDependencies": { "@commander-js/extra-typings": "^14.0.0", - "@types/node": "^25.3.0", + "@types/node": "^26.1.1", "@types/signale": "^1.4.7", "@vitest/coverage-v8": "^4.0.18", "glob": "^13.0.6", "rimraf": "^6.1.3", "tsup": "^8.5.1", - "typescript": "^5.9.3", + "typescript": "^7.0.2", "vitest": "^4.0.18", "zod": "^4.3.6" }, diff --git a/packages/swarm/src/cli/cli-logger.ts b/packages/swarm/src/cli/cli-logger.ts index 4028e139..7b4f633a 100644 --- a/packages/swarm/src/cli/cli-logger.ts +++ b/packages/swarm/src/cli/cli-logger.ts @@ -1,6 +1,6 @@ import type { Signale, SignaleOptions } from 'signale'; import pkg from 'signale'; -import { Logger, LogLevel } from '../common/logger'; +import type { Logger, LogLevel } from '../common/logger'; import { getConfigManager } from '../config'; const { Signale: SignaleConstructor } = pkg; diff --git a/packages/swarm/src/cli/cli-manager.test.ts b/packages/swarm/src/cli/cli-manager.test.ts index cdfed404..c8be4832 100644 --- a/packages/swarm/src/cli/cli-manager.test.ts +++ b/packages/swarm/src/cli/cli-manager.test.ts @@ -1,13 +1,17 @@ import { Command } from 'commander'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { z } from 'zod/v4'; -import { Generator, GeneratorProvider, GeneratorServices } from '../generator'; +import type { + Generator, + GeneratorProvider, + GeneratorServices, +} from '../generator'; import { CLIManager } from './cli-manager'; // Mock generator provider for testing const createMockProvider = (name: string, schema: any): GeneratorProvider => { return { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name, description: `Generate ${name}`, schema, @@ -61,7 +65,7 @@ describe('CommandManager', () => { }); const generateFn = vi.fn().mockResolvedValue(undefined); const provider: GeneratorProvider = { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name: 'test-command', description: 'Generate test-command', schema: testSchema, diff --git a/packages/swarm/src/cli/cli-manager.ts b/packages/swarm/src/cli/cli-manager.ts index 2f4df5f7..facc7e38 100644 --- a/packages/swarm/src/cli/cli-manager.ts +++ b/packages/swarm/src/cli/cli-manager.ts @@ -1,11 +1,11 @@ import { Command } from 'commander'; import { realFileSystem, toKebabCase } from '../common'; -import { GeneratorProvider, getGeneratorServices } from '../generator'; +import { type GeneratorProvider, getGeneratorServices } from '../generator'; import { PluginInterfaceManager } from '../plugin'; import { - SchemaFieldMetadata, + type SchemaFieldMetadata, SchemaManager, - StandardSchemaV1, + type StandardSchemaV1, standardValidate, } from '../schema'; import { getCLILogger } from './cli-logger'; @@ -53,7 +53,7 @@ export class CLIManager extends PluginInterfaceManager { command.action(async (rawArgs: unknown) => { try { await this.executeCommand(name, rawArgs); - } catch (err: any) { + } catch (_err: unknown) { process.exit(1); } }); @@ -82,7 +82,7 @@ export class CLIManager extends PluginInterfaceManager { } private async validateArgs(schema: StandardSchemaV1, rawArgs: unknown) { - const result = await standardValidate(schema, rawArgs as any); + const result = await standardValidate(schema, rawArgs); if (result.issues) { const errorMessages = result.issues .map((issue) => `${issue.message}${this.formatIssuePath(issue.path)}`) diff --git a/packages/swarm/src/cli/index.ts b/packages/swarm/src/cli/index.ts index 3953ed9c..50af4d11 100644 --- a/packages/swarm/src/cli/index.ts +++ b/packages/swarm/src/cli/index.ts @@ -1,5 +1,5 @@ -import { Command } from 'commander'; import fs from 'node:fs'; +import { Command } from 'commander'; import { DEFAULT_CONFIG_FILE, getVersion } from '../common'; import { AppGenerator, getGeneratorServices } from '../generator'; import { getCLILogger } from './cli-logger'; @@ -40,24 +40,32 @@ export async function main(): Promise { '-d, --target-dir [target-dir]', 'Target directory (defaults to project name)' ) - .action(async (name: string, options: any) => { - try { - if (!options.template) { - logger.error( - 'Template is required. Use --template to specify a GitHub repository.' - ); + .action( + async ( + name: string, + options: { template?: string; targetDir?: string | boolean } + ) => { + try { + if (!options.template) { + logger.error( + 'Template is required. Use --template to specify a GitHub repository.' + ); + process.exit(1); + } + await appGen.generate({ + name, + template: options.template, + targetDir: + typeof options.targetDir === 'string' + ? options.targetDir + : undefined, + }); + } catch (err) { + logger.error(err instanceof Error ? err.message : String(err)); process.exit(1); } - await appGen.generate({ - name, - template: options.template, - targetDir: options.targetDir, - }); - } catch (err: any) { - logger.error(err.message); - process.exit(1); } - }); + ); command.addCommand(createCmd); } else { diff --git a/packages/swarm/src/common/errors.ts b/packages/swarm/src/common/errors.ts index 1a0acd31..37a06248 100644 --- a/packages/swarm/src/common/errors.ts +++ b/packages/swarm/src/common/errors.ts @@ -38,6 +38,6 @@ export function handleFatalError( * Used purely to help compiler check for exhaustiveness in switch statements, * will never execute. See https://stackoverflow.com/a/39419171. */ -export function assertUnreachable(x: never): never { +export function assertUnreachable(_x: never): never { throw Error('This code should be unreachable'); } diff --git a/packages/swarm/src/common/version.ts b/packages/swarm/src/common/version.ts index da9c2aee..49cc17b5 100644 --- a/packages/swarm/src/common/version.ts +++ b/packages/swarm/src/common/version.ts @@ -15,9 +15,10 @@ export function getVersion( startDir?: string ): string { const cacheKey = `${packageName}:${startDir ?? 'default'}`; + const cached = versionCache.get(cacheKey); - if (versionCache.has(cacheKey)) { - return versionCache.get(cacheKey)!; + if (cached !== undefined) { + return cached; } const searchDir = startDir ?? path.dirname(fileURLToPath(import.meta.url)); diff --git a/packages/swarm/src/config/config-manager.ts b/packages/swarm/src/config/config-manager.ts index d269bc48..a5d25dd6 100644 --- a/packages/swarm/src/config/config-manager.ts +++ b/packages/swarm/src/config/config-manager.ts @@ -1,12 +1,12 @@ -import { AsyncSearcher, LilconfigResult, lilconfig } from 'lilconfig'; import * as fs from 'node:fs'; import * as path from 'node:path'; +import { type AsyncSearcher, type LilconfigResult, lilconfig } from 'lilconfig'; import { DEFAULT_CONFIG_FILE, DEFAULT_CUSTOM_TEMPLATES_DIR, - LogLevel, findPackageJson, hasWorkspaceConfig, + type LogLevel, } from '../common'; /** @@ -150,7 +150,7 @@ class ConfigManager { return currentDir; } } - } catch (e) { + } catch (_e) { // Ignore package.json parsing errors } } @@ -197,7 +197,7 @@ class ConfigManager { result = await this.lilconfig.search(searchDir); } - if (!result || !result.config) { + if (!result?.config) { // Use default configuration when no config file is found console.warn( `No configuration file found in ${searchDir}. Searched for: ${this.searchPlaces.join(', ')}` @@ -209,23 +209,21 @@ class ConfigManager { return this.config; } - this.config = result.config; + const config: Config = result.config; + this.config = config; this.configPath = result.filepath || null; - if (this.config && !this.config.templateDirectory) { - this.config.templateDirectory = DEFAULT_CUSTOM_TEMPLATES_DIR; + if (!config.templateDirectory) { + config.templateDirectory = DEFAULT_CUSTOM_TEMPLATES_DIR; } // Check if no plugins are defined and warn the user - if ( - this.config && - (!this.config.plugins || this.config.plugins.length === 0) - ) { + if (!config.plugins || config.plugins.length === 0) { console.warn('No plugins are defined in the configuration file.'); console.warn('Swarm will not have any generators available.'); } - return this.config!; + return config; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; @@ -256,7 +254,7 @@ class ConfigManager { */ getLogLevel(): LogLevel { // First check environment variable - const envLevel = process.env['SWARM_MCP_LOG_LEVEL']; + const envLevel = process.env.SWARM_MCP_LOG_LEVEL; if (envLevel && ['debug', 'info', 'warn', 'error'].includes(envLevel)) { return envLevel as LogLevel; } diff --git a/packages/swarm/src/config/index.ts b/packages/swarm/src/config/index.ts index 3f8c326a..33bd9c85 100644 --- a/packages/swarm/src/config/index.ts +++ b/packages/swarm/src/config/index.ts @@ -1,2 +1,2 @@ -export { getConfigManager } from './config-manager'; export type { Config } from './config-manager'; +export { getConfigManager } from './config-manager'; diff --git a/packages/swarm/src/generator/app/app-generator.test.ts b/packages/swarm/src/generator/app/app-generator.test.ts index e607ad6a..14171974 100644 --- a/packages/swarm/src/generator/app/app-generator.test.ts +++ b/packages/swarm/src/generator/app/app-generator.test.ts @@ -1,10 +1,14 @@ import path from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { FileSystem, toFriendlyName, validateProjectName } from '../../common'; import { - Generator, - GeneratorProvider, - GeneratorServices, + type FileSystem, + toFriendlyName, + validateProjectName, +} from '../../common'; +import { + type Generator, + type GeneratorProvider, + type GeneratorServices, getGeneratorServices, } from '../index'; import { AppGenerator } from './app-generator'; diff --git a/packages/swarm/src/generator/app/app-generator.ts b/packages/swarm/src/generator/app/app-generator.ts index a7cf4bb2..c20897ed 100644 --- a/packages/swarm/src/generator/app/app-generator.ts +++ b/packages/swarm/src/generator/app/app-generator.ts @@ -1,14 +1,13 @@ -import { downloadTemplate } from 'giget'; import path from 'node:path'; +import { downloadTemplate } from 'giget'; import { toFriendlyName, toKebabCase, toPascalCase, validateProjectName, } from '../../common'; -import { Out } from '../../schema'; +import type { Out } from '../../schema'; import { GeneratorBase } from '../generator.base'; -import { GeneratorServices } from '../services'; import { schema } from './schema'; interface ReplacementContext { @@ -24,10 +23,6 @@ export class AppGenerator extends GeneratorBase { schema = schema; private readonly supportedExtensions = ['.json', '.ts', '.tsx']; - constructor(services: GeneratorServices) { - super(services); - } - async generate(args: Out): Promise { return this.handleGeneratorError('project', args.name, async () => { const validation = validateProjectName(args.name); diff --git a/packages/swarm/src/generator/generator.base.ts b/packages/swarm/src/generator/generator.base.ts index c4964971..d1d0becd 100644 --- a/packages/swarm/src/generator/generator.base.ts +++ b/packages/swarm/src/generator/generator.base.ts @@ -1,18 +1,18 @@ import path from 'node:path'; -import { FileSystem } from '../common'; -import { Logger } from '../common/logger'; +import type { FileSystem } from '../common'; +import type { Logger } from '../common/logger'; import { - In, - Out, - SchemaFieldMetadata, + type In, + type Out, + type SchemaFieldMetadata, SchemaManager, - StandardSchemaV1, + type StandardSchemaV1, StandardValidationError, - ValidationResult, standardValidate, + type ValidationResult, } from '../schema'; -import { GeneratorServices } from './services'; -import { Generator } from './types'; +import type { GeneratorServices } from './services'; +import type { Generator } from './types'; /** * Abstract base class for all generators @@ -60,7 +60,7 @@ export abstract class GeneratorBase issues: result.issues, errors: result.issues.map((issue) => issue.message), }; - } catch (error: any) { + } catch (error) { if (error instanceof StandardValidationError) { return { valid: false, @@ -123,7 +123,7 @@ export abstract class GeneratorBase this.logCompletion(itemType, itemName); return result; - } catch (error: any) { + } catch (error) { this.logger.error( `Generating ${itemName} ${itemType.toLowerCase()} failed.` ); diff --git a/packages/swarm/src/generator/index.ts b/packages/swarm/src/generator/index.ts index c96901c5..55975a73 100644 --- a/packages/swarm/src/generator/index.ts +++ b/packages/swarm/src/generator/index.ts @@ -1,9 +1,9 @@ export { AppGenerator } from './app'; export { GeneratorBase } from './generator.base'; -export type { GeneratorServices } from './services'; -export { getGeneratorServices } from './services'; export type { GeneratorProvider } from './provider'; export { createProviders } from './provider'; -export { createGenerator } from './testing'; +export type { GeneratorServices } from './services'; +export { getGeneratorServices } from './services'; export { TemplateResolver } from './template-resolver'; +export { createGenerator } from './testing'; export * from './types'; diff --git a/packages/swarm/src/generator/provider.ts b/packages/swarm/src/generator/provider.ts index 53099a0a..5a70e307 100644 --- a/packages/swarm/src/generator/provider.ts +++ b/packages/swarm/src/generator/provider.ts @@ -1,5 +1,5 @@ -import { GeneratorServices } from './services'; -import { Generator } from './types'; +import type { GeneratorServices } from './services'; +import type { Generator } from './types'; /** * Provider interface for creating a generator instances with injected services @@ -13,15 +13,31 @@ export interface GeneratorProvider { create: (services: GeneratorServices) => Generator | Promise; } +/** + * Schema placeholder for generator types that must accept any concrete schema. + * + * `Generator` is invariant over its schema type (the schema appears in both + * covariant and contravariant positions), so a concrete `Generator` + * is not assignable to `Generator`. `any` is required to + * accept generator classes for any concrete schema. + */ +// biome-ignore lint/suspicious/noExplicitAny: see note above on schema invariance +type AnySchema = any; + +/** + * Constructor for a generator of any schema. + */ +export type GeneratorConstructor = new ( + services: GeneratorServices +) => Generator; + /** * Creates providers for the given generator classes * @param generatorClasses - Array of generator class constructors * @returns Array of generator providers */ export function createProviders( - ...generatorClasses: Array< - new (services: GeneratorServices) => Generator - > + ...generatorClasses: Array ): Array { return generatorClasses.map( (GeneratorClass): GeneratorProvider => ({ diff --git a/packages/swarm/src/generator/services.ts b/packages/swarm/src/generator/services.ts index b6136703..e26027ee 100644 --- a/packages/swarm/src/generator/services.ts +++ b/packages/swarm/src/generator/services.ts @@ -1,5 +1,5 @@ -import { FileSystem, Logger, realFileSystem } from '../common'; -import { GeneratorEnvironment } from './types'; +import { type FileSystem, type Logger, realFileSystem } from '../common'; +import type { GeneratorEnvironment } from './types'; export interface GeneratorServices { fileSystem: FileSystem; @@ -7,7 +7,7 @@ export interface GeneratorServices { } export function getGeneratorServices( - env: GeneratorEnvironment, + _env: GeneratorEnvironment, logger: Logger, overrides: Partial = {} ): GeneratorServices { diff --git a/packages/swarm/src/generator/template-resolver.test.ts b/packages/swarm/src/generator/template-resolver.test.ts index 1f8875c1..0336eeff 100644 --- a/packages/swarm/src/generator/template-resolver.test.ts +++ b/packages/swarm/src/generator/template-resolver.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { DEFAULT_CUSTOM_TEMPLATES_DIR, FileSystem } from '../common'; +import { DEFAULT_CUSTOM_TEMPLATES_DIR, type FileSystem } from '../common'; import { TemplateResolver } from './template-resolver'; // Mock file system diff --git a/packages/swarm/src/generator/template-resolver.ts b/packages/swarm/src/generator/template-resolver.ts index 0c086bf3..9a40387e 100644 --- a/packages/swarm/src/generator/template-resolver.ts +++ b/packages/swarm/src/generator/template-resolver.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { DEFAULT_CUSTOM_TEMPLATES_DIR, FileSystem } from '../common'; +import { DEFAULT_CUSTOM_TEMPLATES_DIR, type FileSystem } from '../common'; export class TemplateResolver { constructor(private fileSystem: FileSystem) {} diff --git a/packages/swarm/src/generator/testing.ts b/packages/swarm/src/generator/testing.ts index 06622fe9..6d63eb4c 100644 --- a/packages/swarm/src/generator/testing.ts +++ b/packages/swarm/src/generator/testing.ts @@ -1,6 +1,6 @@ -import { GeneratorBase } from './generator.base'; -import { GeneratorProvider } from './provider'; -import { GeneratorServices, getGeneratorServices } from './services'; +import type { GeneratorBase } from './generator.base'; +import type { GeneratorProvider } from './provider'; +import { type GeneratorServices, getGeneratorServices } from './services'; /** * Create a generator instance from a provider with service overrides for testing. @@ -19,6 +19,7 @@ import { GeneratorServices, getGeneratorServices } from './services'; * ); * ``` */ +// biome-ignore lint/suspicious/noExplicitAny: constraint must accept generators of any concrete schema; GeneratorBase is invariant over its schema type export async function createGenerator>( provider: GeneratorProvider, overrides: Partial = {} diff --git a/packages/swarm/src/generator/types.ts b/packages/swarm/src/generator/types.ts index 2ba7d1ad..05cb271c 100644 --- a/packages/swarm/src/generator/types.ts +++ b/packages/swarm/src/generator/types.ts @@ -1,5 +1,4 @@ -import type { StandardSchemaV1 } from '../schema'; -import { Out } from '../schema'; +import type { Out, StandardSchemaV1 } from '../schema'; export type GeneratorEnvironment = 'cli' | 'mcp' | 'test'; diff --git a/packages/swarm/src/mcp/cli/commands/start.test.ts b/packages/swarm/src/mcp/cli/commands/start.test.ts index 38db6ea8..c9838570 100644 --- a/packages/swarm/src/mcp/cli/commands/start.test.ts +++ b/packages/swarm/src/mcp/cli/commands/start.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ServerManager } from '../server-manager'; +import type { ServerManager } from '../server-manager'; import { createStartCommand } from './start'; // Mock the ServerManager diff --git a/packages/swarm/src/mcp/cli/commands/start.ts b/packages/swarm/src/mcp/cli/commands/start.ts index 60a9c0e4..83bbacbe 100644 --- a/packages/swarm/src/mcp/cli/commands/start.ts +++ b/packages/swarm/src/mcp/cli/commands/start.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { getCLILogger } from '../../../cli/cli-logger'; -import { ServerManager } from '../server-manager'; +import type { ServerManager } from '../server-manager'; export function createStartCommand(serverManager: ServerManager): Command { const logger = getCLILogger(); diff --git a/packages/swarm/src/mcp/cli/commands/status.test.ts b/packages/swarm/src/mcp/cli/commands/status.test.ts index 393a8982..34dc4ede 100644 --- a/packages/swarm/src/mcp/cli/commands/status.test.ts +++ b/packages/swarm/src/mcp/cli/commands/status.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ServerManager } from '../server-manager'; +import type { ServerManager } from '../server-manager'; import { createStatusCommand } from './status'; // Mock the ServerManager diff --git a/packages/swarm/src/mcp/cli/commands/status.ts b/packages/swarm/src/mcp/cli/commands/status.ts index 1666d91b..c7d14342 100644 --- a/packages/swarm/src/mcp/cli/commands/status.ts +++ b/packages/swarm/src/mcp/cli/commands/status.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { getCLILogger } from '../../../cli/cli-logger'; -import { ServerManager } from '../server-manager'; +import type { ServerManager } from '../server-manager'; interface StatusOptions { json?: boolean; diff --git a/packages/swarm/src/mcp/cli/commands/stop.test.ts b/packages/swarm/src/mcp/cli/commands/stop.test.ts index 2cd7f7e4..18eeea68 100644 --- a/packages/swarm/src/mcp/cli/commands/stop.test.ts +++ b/packages/swarm/src/mcp/cli/commands/stop.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ServerManager } from '../server-manager'; +import type { ServerManager } from '../server-manager'; import { createStopCommand } from './stop'; // Mock the ServerManager diff --git a/packages/swarm/src/mcp/cli/commands/stop.ts b/packages/swarm/src/mcp/cli/commands/stop.ts index adfce6fb..4744904a 100644 --- a/packages/swarm/src/mcp/cli/commands/stop.ts +++ b/packages/swarm/src/mcp/cli/commands/stop.ts @@ -1,6 +1,6 @@ import { Command } from 'commander'; import { getCLILogger } from '../../../cli/cli-logger'; -import { ServerManager } from '../server-manager'; +import type { ServerManager } from '../server-manager'; interface StopOptions { force?: boolean; diff --git a/packages/swarm/src/mcp/cli/server-manager.test.ts b/packages/swarm/src/mcp/cli/server-manager.test.ts index 81820568..ebed83ba 100644 --- a/packages/swarm/src/mcp/cli/server-manager.test.ts +++ b/packages/swarm/src/mcp/cli/server-manager.test.ts @@ -232,12 +232,12 @@ describe('ServerManager', () => { }); describe('resolveProjectRoot', () => { - let originalCwd: string; + let _originalCwd: string; let originalArgv: string[]; const mockedExistsSync = vi.mocked(existsSync); beforeEach(() => { - originalCwd = process.cwd(); + _originalCwd = process.cwd(); originalArgv = [...process.argv]; mockedExistsSync.mockReset(); }); diff --git a/packages/swarm/src/mcp/cli/server-manager.ts b/packages/swarm/src/mcp/cli/server-manager.ts index 715af99a..b1ff678f 100644 --- a/packages/swarm/src/mcp/cli/server-manager.ts +++ b/packages/swarm/src/mcp/cli/server-manager.ts @@ -8,7 +8,7 @@ import { createErrorContext, InternalError, MCPManager, - MCPServerConfig, + type MCPServerConfig, } from '../server'; export class ServerManager { diff --git a/packages/swarm/src/mcp/mcp-logger.ts b/packages/swarm/src/mcp/mcp-logger.ts index 9243e704..bb1b5ce8 100644 --- a/packages/swarm/src/mcp/mcp-logger.ts +++ b/packages/swarm/src/mcp/mcp-logger.ts @@ -1,5 +1,5 @@ import type { Server as MCPServer } from '@modelcontextprotocol/sdk/server'; -import { Logger, LogLevel } from '../common/logger'; +import type { Logger, LogLevel } from '../common/logger'; import { getConfigManager } from '../config'; export class MCPLogger implements Logger { @@ -63,11 +63,17 @@ export class MCPLogger implements Logger { } try { + const sendLoggingMessage = ( + this.mcpServer as { + sendLoggingMessage?: (params: { + level: string; + data: unknown; + }) => Promise; + } | null + )?.sendLoggingMessage; + // Check if sendLoggingMessage method exists on the server - if ( - !this.mcpServer || - typeof (this.mcpServer as any).sendLoggingMessage !== 'function' - ) { + if (typeof sendLoggingMessage !== 'function') { // Fallback to stderr if method doesn't exist const logMessage = this.formatMessage(level, message, context); @@ -95,11 +101,11 @@ export class MCPLogger implements Logger { const mcpLevel = levelMap[level] || 'info'; - await (this.mcpServer as any).sendLoggingMessage({ + await sendLoggingMessage({ level: mcpLevel, data: message, }); - } catch (error) { + } catch (_error) { // If notification fails, fallback to stderr to ensure message is not lost // This can happen if the server isn't fully ready or if sendLoggingMessage fails const logMessage = this.formatMessage(level, message, context); diff --git a/packages/swarm/src/mcp/server/errors/app-error.ts b/packages/swarm/src/mcp/server/errors/app-error.ts index 7516757e..39b5fa93 100644 --- a/packages/swarm/src/mcp/server/errors/app-error.ts +++ b/packages/swarm/src/mcp/server/errors/app-error.ts @@ -1,4 +1,4 @@ -import { MCPErrorCode, MCPProtocolError } from './mcp-protocol-error'; +import { type MCPErrorCode, MCPProtocolError } from './mcp-protocol-error'; /** * Error context interface diff --git a/packages/swarm/src/mcp/server/errors/configuration-error.ts b/packages/swarm/src/mcp/server/errors/configuration-error.ts index 07a8e402..862fae63 100644 --- a/packages/swarm/src/mcp/server/errors/configuration-error.ts +++ b/packages/swarm/src/mcp/server/errors/configuration-error.ts @@ -1,4 +1,4 @@ -import { AppError, ErrorContext } from './app-error'; +import { AppError, type ErrorContext } from './app-error'; import { MCPErrorCode } from './mcp-protocol-error'; /** diff --git a/packages/swarm/src/mcp/server/errors/errors.ts b/packages/swarm/src/mcp/server/errors/errors.ts index 7e214a74..1aa5e605 100644 --- a/packages/swarm/src/mcp/server/errors/errors.ts +++ b/packages/swarm/src/mcp/server/errors/errors.ts @@ -1,4 +1,4 @@ -import { ErrorContext } from './app-error'; +import type { ErrorContext } from './app-error'; /** * Create an error context diff --git a/packages/swarm/src/mcp/server/errors/internal-error.ts b/packages/swarm/src/mcp/server/errors/internal-error.ts index b7bf8179..6041b1ca 100644 --- a/packages/swarm/src/mcp/server/errors/internal-error.ts +++ b/packages/swarm/src/mcp/server/errors/internal-error.ts @@ -1,4 +1,4 @@ -import { AppError, ErrorContext } from './app-error'; +import { AppError, type ErrorContext } from './app-error'; import { MCPErrorCode } from './mcp-protocol-error'; /** diff --git a/packages/swarm/src/mcp/server/mcp-manager.ts b/packages/swarm/src/mcp/server/mcp-manager.ts index e492e31d..78f9304d 100644 --- a/packages/swarm/src/mcp/server/mcp-manager.ts +++ b/packages/swarm/src/mcp/server/mcp-manager.ts @@ -1,6 +1,7 @@ +import { randomUUID } from 'node:crypto'; import { Server as MCPServer, - ServerOptions, + type ServerOptions, } from '@modelcontextprotocol/sdk/server'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import type { Transport as MCPTransport } from '@modelcontextprotocol/sdk/shared/transport.js'; @@ -9,12 +10,11 @@ import { ListResourcesRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; -import { randomUUID } from 'crypto'; import { realFileSystem } from '../../common'; import { getMCPLogger } from '../mcp-logger'; import { createErrorContext, InternalError } from './errors'; import { ToolManager } from './tool-manager'; -import { MCPServerConfig, MCPServerInfo, MCPServerState } from './types'; +import type { MCPServerConfig, MCPServerInfo, MCPServerState } from './types'; export class MCPManager { private mcpServer: MCPServer; diff --git a/packages/swarm/src/mcp/server/tool-manager.test.ts b/packages/swarm/src/mcp/server/tool-manager.test.ts index c072f3de..79d80b7a 100644 --- a/packages/swarm/src/mcp/server/tool-manager.test.ts +++ b/packages/swarm/src/mcp/server/tool-manager.test.ts @@ -1,13 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { +import type { Generator, GeneratorProvider, GeneratorServices, } from '../../generator'; import { registerSchemaMetadata, - SchemaMetadata, - StandardSchemaV1, + type SchemaMetadata, + type StandardSchemaV1, } from '../../schema'; import { ToolManager } from './tool-manager'; @@ -66,7 +66,7 @@ describe('ToolManager', () => { }); const mockProviders: GeneratorProvider[] = [ { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name: 'api', description: 'Generate API endpoint', schema: apiSchema, @@ -74,7 +74,7 @@ describe('ToolManager', () => { }), }, { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name: 'crud', description: 'Generate CRUD operations', schema: crudSchema, @@ -143,7 +143,7 @@ describe('ToolManager', () => { }, }); const mockProvider: GeneratorProvider = { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name: 'test', description: 'Test generator', schema: testSchema, @@ -203,7 +203,7 @@ describe('ToolManager', () => { }, }); const mockProvider: GeneratorProvider = { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name: 'api', description: 'Generate API', schema: apiSchema, @@ -229,7 +229,9 @@ describe('ToolManager', () => { expect(result.content[0]).toHaveProperty('type', 'text'); expect(result.content[0]).toHaveProperty('text'); - const parsedText = JSON.parse(result.content[0].text); + const parsedText = JSON.parse( + (result.content[0] as { text: string }).text + ); expect(parsedText.success).toBe(true); expect(generateFn).toHaveBeenCalledWith({ name: 'test-api' }); }); @@ -244,7 +246,7 @@ describe('ToolManager', () => { }, }); const mockProvider: GeneratorProvider = { - create: (services: GeneratorServices): Generator => ({ + create: (_services: GeneratorServices): Generator => ({ name: 'api', description: 'Generate API', schema: apiSchema, diff --git a/packages/swarm/src/mcp/server/tool-manager.ts b/packages/swarm/src/mcp/server/tool-manager.ts index 2a6d4992..6a5f04e0 100644 --- a/packages/swarm/src/mcp/server/tool-manager.ts +++ b/packages/swarm/src/mcp/server/tool-manager.ts @@ -1,15 +1,16 @@ import type { Server as MCPServer } from '@modelcontextprotocol/sdk/server'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import { - Generator, - GeneratorProvider, - GeneratorServices, + type Generator, + type GeneratorProvider, + type GeneratorServices, getGeneratorServices, } from '../../generator'; import { PluginInterfaceManager } from '../../plugin'; import { - SchemaFieldMetadata, + type SchemaFieldMetadata, SchemaManager, - StandardSchemaV1, + type StandardSchemaV1, standardValidate, } from '../../schema'; import { getMCPLogger } from '../mcp-logger'; @@ -22,7 +23,7 @@ interface MCPToolDefinition { description: string; inputSchema: { type: 'object'; - properties: Record; + properties: Record; required?: string[]; }; } @@ -30,7 +31,7 @@ interface MCPToolDefinition { /** * MCP Tool handler function type */ -type MCPToolHandler = (args: any) => Promise; +type MCPToolHandler = (args: unknown) => Promise; /** * MCP Tool interface combining definition and handler @@ -74,7 +75,7 @@ export class ToolManager extends PluginInterfaceManager { throw new Error(`Invalid schema for generator '${generator.name}'`); } - const properties: Record = {}; + const properties: Record = {}; const required: string[] = []; Object.entries(shape).forEach(([fieldName, fieldSchema]) => { @@ -117,7 +118,7 @@ export class ToolManager extends PluginInterfaceManager { * Returns SDK-compatible CallToolResult format */ private createToolHandler(provider: GeneratorProvider): MCPToolHandler { - return async (args: any) => { + return async (args: unknown) => { const services = this.createGeneratorServices(); const generator = await provider.create(services); const validatedArgs = await this.validateArgs(generator.schema, args); @@ -160,7 +161,7 @@ export class ToolManager extends PluginInterfaceManager { private convertFieldToJSONSchema( fieldSchema: SchemaFieldMetadata, metadata?: SchemaFieldMetadata - ): Record { + ): Record { const typeName = SchemaManager.getFieldTypeName(fieldSchema); const description = metadata?.description || ''; @@ -233,7 +234,7 @@ export class ToolManager extends PluginInterfaceManager { } private async validateArgs(schema: StandardSchemaV1, rawArgs: unknown) { - const result = await standardValidate(schema, rawArgs as any); + const result = await standardValidate(schema, rawArgs); if (result.issues) { const message = result.issues .map((issue) => `${issue.message}${this.formatIssuePath(issue.path)}`) @@ -297,9 +298,7 @@ export class ToolManager extends PluginInterfaceManager { * Get all available tools as a single object for MCP server * This is a convenience method that combines definitions and handlers */ - async getTools( - configPath?: string - ): Promise Promise>> { + async getTools(configPath?: string): Promise> { if (!this.isInitialized()) { await this.initialize(configPath); } diff --git a/packages/swarm/src/mcp/server/types/mcp.test.ts b/packages/swarm/src/mcp/server/types/mcp.test.ts index 7f98f565..22af0a4a 100644 --- a/packages/swarm/src/mcp/server/types/mcp.test.ts +++ b/packages/swarm/src/mcp/server/types/mcp.test.ts @@ -5,11 +5,11 @@ import { isNotification, isRequest, isResponse, - MCPNotification, + type MCPNotification, MCPNotificationSchema, - MCPRequest, + type MCPRequest, MCPRequestSchema, - MCPResponse, + type MCPResponse, MCPResponseSchema, } from './mcp.test-utils.js'; diff --git a/packages/swarm/src/mcp/server/types/mcp.ts b/packages/swarm/src/mcp/server/types/mcp.ts index f9c97238..e04df38f 100644 --- a/packages/swarm/src/mcp/server/types/mcp.ts +++ b/packages/swarm/src/mcp/server/types/mcp.ts @@ -22,7 +22,7 @@ export interface MCPServerConfig { description: string; inputSchema: { type: 'object'; - properties: Record; + properties: Record; required?: string[]; additionalProperties?: boolean; }; @@ -35,16 +35,16 @@ export interface MCPServerConfig { export interface MCPServerState { isRunning: boolean; - transport?: any; - clientCapabilities?: any; - clientVersion?: any; + transport?: unknown; + clientCapabilities?: unknown; + clientVersion?: unknown; sessionId?: string | undefined; } export interface MCPServerInfo { name: string; version: string; - capabilities?: any; + capabilities?: unknown; instructions?: string | undefined; status: MCPServerState; } diff --git a/packages/swarm/src/plugin/plugin-interface-manager.ts b/packages/swarm/src/plugin/plugin-interface-manager.ts index 6c6280b0..294230aa 100644 --- a/packages/swarm/src/plugin/plugin-interface-manager.ts +++ b/packages/swarm/src/plugin/plugin-interface-manager.ts @@ -1,6 +1,6 @@ import { getCLILogger } from '../cli/cli-logger'; import { realFileSystem } from '../common'; -import { GeneratorProvider, GeneratorServices } from '../generator'; +import type { GeneratorProvider, GeneratorServices } from '../generator'; import { PluginManager } from './plugin-manager'; /** diff --git a/packages/swarm/src/plugin/plugin-manager.ts b/packages/swarm/src/plugin/plugin-manager.ts index d954ac6c..20fcbef2 100644 --- a/packages/swarm/src/plugin/plugin-manager.ts +++ b/packages/swarm/src/plugin/plugin-manager.ts @@ -1,8 +1,13 @@ import * as path from 'node:path'; +import type { FileSystem } from '../common'; import { getConfigManager } from '../config'; -import { Generator, GeneratorProvider, GeneratorServices } from '../generator'; +import type { + Generator, + GeneratorProvider, + GeneratorServices, +} from '../generator'; import { PluginResolver } from './plugin-resolver'; -import { Plugin } from './types'; +import type { Plugin } from './types'; /** * Main plugin management system @@ -32,7 +37,7 @@ export class PluginManager { for (const provider of plugin.providers) { const tempServices: GeneratorServices = { - fileSystem: {} as any, + fileSystem: {} as unknown as FileSystem, logger: { debug: () => {}, info: () => {}, diff --git a/packages/swarm/src/plugin/plugin-resolver.ts b/packages/swarm/src/plugin/plugin-resolver.ts index d15e963f..4f292930 100644 --- a/packages/swarm/src/plugin/plugin-resolver.ts +++ b/packages/swarm/src/plugin/plugin-resolver.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { findPackageDirectory } from '../common/package-utils'; -import { Plugin } from './types'; +import type { Plugin } from './types'; /** * Unified plugin resolver using dynamic imports @@ -95,11 +95,11 @@ export class PluginResolver { applicationRoot: string ): Promise { try { - const { createRequire } = await import('module'); + const { createRequire } = await import('node:module'); const require = createRequire(path.join(applicationRoot, 'package.json')); const resolvedPath = require.resolve(packageName); return resolvedPath; - } catch (error) { + } catch (_error) { // Fallback to manual resolution const packagePath = await this.resolvePackagePath( packageName, @@ -137,7 +137,7 @@ export class PluginResolver { ): Promise { try { try { - const { createRequire } = await import('module'); + const { createRequire } = await import('node:module'); const require = createRequire( path.join(applicationRoot, 'package.json') ); @@ -150,7 +150,7 @@ export class PluginResolver { if (packageDir) { return packageDir; } - } catch (resolveError) { + } catch (_resolveError) { // Fall through to manual resolution } @@ -183,17 +183,21 @@ export class PluginResolver { * @param plugin Plugin object to validate * @returns Validated plugin or null */ - private validatePlugin(plugin: any): plugin is Plugin { + private validatePlugin(plugin: unknown): plugin is Plugin { + if (plugin === null || typeof plugin !== 'object') { + return false; + } + + const candidate = plugin as { name?: unknown; providers?: unknown }; + return ( - plugin !== null && - typeof plugin === 'object' && - typeof plugin.name === 'string' && - Array.isArray(plugin.providers) && - plugin.providers.every( - (provider: any) => - provider && + typeof candidate.name === 'string' && + Array.isArray(candidate.providers) && + candidate.providers.every( + (provider: unknown) => + provider !== null && typeof provider === 'object' && - typeof provider.create === 'function' + typeof (provider as { create?: unknown }).create === 'function' ) ); } diff --git a/packages/swarm/src/plugin/plugin.ts b/packages/swarm/src/plugin/plugin.ts index 5b119c49..a06056b0 100644 --- a/packages/swarm/src/plugin/plugin.ts +++ b/packages/swarm/src/plugin/plugin.ts @@ -1,7 +1,8 @@ -import { createProviders } from '../generator/provider'; -import { GeneratorServices } from '../generator/services'; -import { Generator } from '../generator/types'; -import { Plugin } from './types'; +import { + createProviders, + type GeneratorConstructor, +} from '../generator/provider'; +import type { Plugin } from './types'; /** * Creates a plugin with the given name and generator classes @@ -20,9 +21,7 @@ import { Plugin } from './types'; */ export function createPlugin( name: string, - ...generatorClasses: Array< - new (services: GeneratorServices) => Generator - > + ...generatorClasses: Array ): Plugin { return { name, diff --git a/packages/swarm/src/plugin/types.ts b/packages/swarm/src/plugin/types.ts index 75fabf32..76e43228 100644 --- a/packages/swarm/src/plugin/types.ts +++ b/packages/swarm/src/plugin/types.ts @@ -1,4 +1,4 @@ -import { GeneratorProvider } from '../generator/provider'; +import type { GeneratorProvider } from '../generator/provider'; /** * Plugin interface diff --git a/packages/swarm/src/schema/metadata.ts b/packages/swarm/src/schema/metadata.ts index 6a9eb5a1..86aad35b 100644 --- a/packages/swarm/src/schema/metadata.ts +++ b/packages/swarm/src/schema/metadata.ts @@ -53,12 +53,18 @@ type GlobalWithRegistry = typeof globalThis & { const globalWithRegistry = globalThis as GlobalWithRegistry; -const registry = - globalWithRegistry[metadataRegistrySymbol] ?? - (globalWithRegistry[metadataRegistrySymbol] = new WeakMap< - StandardSchemaV1, - SchemaMetadata - >()); +function getOrCreateRegistry(): WeakMap { + const existing = globalWithRegistry[metadataRegistrySymbol]; + if (existing) { + return existing; + } + + const created = new WeakMap(); + globalWithRegistry[metadataRegistrySymbol] = created; + return created; +} + +const registry = getOrCreateRegistry(); export function registerSchemaMetadata( schema: T, diff --git a/packages/swarm/src/schema/schema-manager.test.ts b/packages/swarm/src/schema/schema-manager.test.ts index fd9a5e00..a87a4220 100644 --- a/packages/swarm/src/schema/schema-manager.test.ts +++ b/packages/swarm/src/schema/schema-manager.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { SchemaManager, StandardSchemaV1 } from '.'; -import { registerSchemaMetadata, SchemaMetadata } from './metadata'; +import { SchemaManager, type StandardSchemaV1 } from '.'; +import { registerSchemaMetadata, type SchemaMetadata } from './metadata'; function createSchema(metadata?: SchemaMetadata): StandardSchemaV1 { const schema: StandardSchemaV1 = { diff --git a/packages/swarm/src/schema/schema-manager.ts b/packages/swarm/src/schema/schema-manager.ts index 0507cfa6..2fa236f5 100644 --- a/packages/swarm/src/schema/schema-manager.ts +++ b/packages/swarm/src/schema/schema-manager.ts @@ -3,46 +3,37 @@ import { getSchemaMetadata } from './metadata'; import type { StandardSchemaV1 } from './standard'; /** - * Utility class for working with Standard Schema metadata. + * Utilities for working with Standard Schema metadata. */ -export class SchemaManager { - static getShape( - schema: StandardSchemaV1 - ): SchemaMetadata['fields'] | undefined { +export const SchemaManager = { + getShape(schema: StandardSchemaV1): SchemaMetadata['fields'] | undefined { return getSchemaMetadata(schema)?.fields; - } + }, - static getCommandMetadata( + getCommandMetadata( field: SchemaFieldMetadata ): SchemaFieldMetadata | undefined { return field; - } + }, - static isFieldRequired(field: SchemaFieldMetadata): boolean { + isFieldRequired(field: SchemaFieldMetadata): boolean { return field.required ?? true; - } + }, - static getFieldTypeName(field: SchemaFieldMetadata): string { + getFieldTypeName(field: SchemaFieldMetadata): string { return field.type; - } + }, - static getArrayElement( - field: SchemaFieldMetadata - ): SchemaFieldMetadata | undefined { + getArrayElement(field: SchemaFieldMetadata): SchemaFieldMetadata | undefined { return field.elementType; - } + }, - static getEnumValues( - field: SchemaFieldMetadata - ): (string | number)[] | undefined { + getEnumValues(field: SchemaFieldMetadata): (string | number)[] | undefined { return field.enumValues; - } + }, - static findEnumValue( - enumValues: Record, - value: string - ): string { + findEnumValue(enumValues: Record, value: string): string { const mapped = enumValues[value.toUpperCase() as keyof typeof enumValues]; return mapped || value; - } -} + }, +}; diff --git a/packages/swarm/src/schema/standard.ts b/packages/swarm/src/schema/standard.ts index 94c79f1e..bb674df7 100644 --- a/packages/swarm/src/schema/standard.ts +++ b/packages/swarm/src/schema/standard.ts @@ -7,11 +7,16 @@ type StandardSchemaResult = StandardSchemaV1.Result< >; export function isStandardSchema(value: unknown): value is StandardSchemaV1 { + if (typeof value !== 'object' || value === null) { + return false; + } + + const standard = (value as { '~standard'?: unknown })['~standard']; + return ( - typeof value === 'object' && - value !== null && - typeof (value as any)['~standard'] === 'object' && - typeof (value as any)['~standard'].validate === 'function' + typeof standard === 'object' && + standard !== null && + typeof (standard as { validate?: unknown }).validate === 'function' ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ba77787..52fe6303 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,36 +8,21 @@ importers: .: devDependencies: + '@biomejs/biome': + specifier: 2.5.3 + version: 2.5.3 '@changesets/cli': - specifier: ^2.29.7 - version: 2.29.7(@types/node@24.10.1) - '@eslint/js': - specifier: ^9.39.1 - version: 9.39.1 + specifier: ^2.31.0 + version: 2.31.0(@types/node@26.1.1) '@types/node': - specifier: ^24.10.1 - version: 24.10.1 - '@typescript-eslint/eslint-plugin': - specifier: ^8.46.4 - version: 8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^8.46.4 - version: 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint: - specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) - eslint-config-prettier: - specifier: ^10.1.8 - version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + specifier: ^26.1.1 + version: 26.1.1 knip: - specifier: ^5.69.1 - version: 5.69.1(@types/node@24.10.1)(typescript@5.9.3) - prettier: - specifier: ^3.6.2 - version: 3.6.2 + specifier: ^6.26.0 + version: 6.26.0 typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 packages/swarm: dependencies: @@ -64,14 +49,14 @@ importers: specifier: ^14.0.0 version: 14.0.0(commander@14.0.3) '@types/node': - specifier: ^25.3.0 - version: 25.3.0 + specifier: ^26.1.1 + version: 26.1.1 '@types/signale': specifier: ^1.4.7 version: 1.4.7 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6)) + version: 4.0.18(vitest@4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0)) glob: specifier: ^13.0.6 version: 13.0.6 @@ -80,13 +65,13 @@ importers: version: 6.1.3 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) + version: 8.5.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(typescript@7.0.2)(yaml@2.9.0) typescript: - specifier: ^5.9.3 - version: 5.9.3 + specifier: ^7.0.2 + version: 7.0.2 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6) + version: 4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0) zod: specifier: ^4.3.6 version: 4.3.6 @@ -105,16 +90,22 @@ importers: semver: specifier: ^7.7.4 version: 7.7.4 + typescript: + specifier: ^5.9.0 + version: 5.9.3 devDependencies: '@types/node': - specifier: ^25.3.0 - version: 25.3.0 + specifier: ^26.1.1 + version: 26.1.1 '@types/semver': specifier: ^7.7.1 version: 7.7.1 '@vitest/coverage-v8': specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6)) + version: 4.0.18(vitest@4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0)) + '@wasp.sh/spec': + specifier: link:src/wasp-config/stubs + version: link:src/wasp-config/stubs glob: specifier: ^13.0.6 version: 13.0.6 @@ -123,16 +114,10 @@ importers: version: 6.1.3 tsup: specifier: ^8.5.1 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.9.0) vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6) - wasp-config: - specifier: link:src/wasp-config/stubs - version: link:src/wasp-config/stubs + version: 4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0) zod: specifier: ^4.3.6 version: 4.3.6 @@ -164,30 +149,83 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@changesets/apply-release-plan@7.0.13': - resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} + '@biomejs/biome@2.5.3': + resolution: {integrity: sha512-MrJswFdei9EfDwwUy2tQrPDpK0AO+RmMFvBoaaJ6ayBc3sUbHdCE+XG5N8vp+5So41ZupZJQm0roHFFhMGVD7A==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.5.3': + resolution: {integrity: sha512-QhYP9muVQ0nUO5zztFuPbEwi4+94sJWVjaZds9aMi1l/KNZBiUjdiSUrGHsTaMGDXrYl+r4AS2sUKfgH3w+V3g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.5.3': + resolution: {integrity: sha512-NC1Ss13UaW7QZX+y8j44bF7AP0jSJdBl6iRhe0MAkvaSqZy+mWg3GaXsrb+eSoHoGDBtaXWEbMVV0iVN2cZ7cQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.5.3': + resolution: {integrity: sha512-fccix0w6xp6csCXgxeC0dU/3ecgRQal0y+cv2SP9ajNlhe7Yrk2Ug7UDe2j9AT9ZDYitkXpvUKgZjjuoYeP4Vg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.5.3': + resolution: {integrity: sha512-ksx1KWeyYW18ILL04msF/J4ZBtBDN33znYK8Z/aNv/vlBVxL9/g3mGP+omgHJKy4+KWbK87vcmmpmurfNjSgiA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.5.3': + resolution: {integrity: sha512-O/yU9YKRUiHhmcjF2f38PSjseVk3G4VLWYc0G2HWpzdBVREV6G8IGWIVEFf7MFPfWIzNUIvPsEjeAZQIOgnLcQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.5.3': + resolution: {integrity: sha512-yMkJtilsgvILDcVkh187aVLTb64xYsrxYajx5kym+r1ULkO5HUOfu9AYKLGQbOVLwJtT2utNw7hhFNg+17mUYA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] - '@changesets/assemble-release-plan@6.0.9': - resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + '@biomejs/cli-win32-arm64@2.5.3': + resolution: {integrity: sha512-cX5z+GYwRcqEok0AH3KSfQGgqYd0Nomfp6Fbe1uiTtELE38hdH2k842wQ9wLNaF/JJ7r4rjJQ4VR+ce+fRmQbw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.5.3': + resolution: {integrity: sha512-ExSaJWi4/u6+GXCszlSKpWSjKNbDseAYqqkCznsCsZ/4uidZ/BEqsCc5/3ctlq6dfIubdIIRSVLC/PG9xPl70Q==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@changesets/apply-release-plan@7.1.1': + resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} + + '@changesets/assemble-release-plan@6.0.10': + resolution: {integrity: sha512-rSDcqdJ9KbVyjpBIuCidhvZNIiVt1XaIYp73ycVQRIA5n/j6wQaEk0ChRLMUQ1vkxZe51PTQ9OIhbg6HQMW45A==} '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.29.7': - resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} + '@changesets/cli@2.31.0': + resolution: {integrity: sha512-AhI4enNTgHu2IZr6K4WZyf0EPch4XVMn1yOMFmCD9gsfBGqMYaHXls5HyDv6/CL5axVQABz68eG30eCtbr2wFg==} hasBin: true - '@changesets/config@3.1.1': - resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + '@changesets/config@3.1.4': + resolution: {integrity: sha512-pf0bvD/v6WI2cRlZ6hzpjtZdSlXDXMAJ+Iz7xfFzV4ZxJ8OGGAON+1qYc99ZPrijnt4xp3VGG7eNvAOGS24V1Q==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - '@changesets/get-dependents-graph@2.1.3': - resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + '@changesets/get-dependents-graph@2.1.4': + resolution: {integrity: sha512-ZsS00x6WvmHq3sQv8oCMwL0f/z3wbXCVuSVTJwCnnmbC/iBdNJGFx1EcbMG4PC6sXRyH69liM4A2WKXzn/kRPg==} - '@changesets/get-release-plan@4.0.13': - resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + '@changesets/get-release-plan@4.0.16': + resolution: {integrity: sha512-2K5Om6CrMPm45rtvckfzWo7e9jOVCKLCnXia5eUPaURH7/LWzri7pK1TycdzAuAtehLkW7VPbWLCSExTHmiI6g==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -198,14 +236,14 @@ packages: '@changesets/logger@0.1.1': resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - '@changesets/parse@0.4.1': - resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} '@changesets/pre@2.0.2': resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - '@changesets/read@0.6.5': - resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} '@changesets/should-skip-package@0.1.2': resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} @@ -239,14 +277,20 @@ packages: peerDependencies: commander: ~14.0.0 - '@emnapi/core@1.7.1': - resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} + '@emnapi/core@1.11.0': + resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==} - '@emnapi/runtime@1.7.1': - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/runtime@1.11.0': + resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} @@ -560,66 +604,12 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.1': - resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@hono/node-server@1.19.9': resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -662,8 +652,11 @@ packages: resolution: {integrity: sha512-nKouX7rsrzk/5nk6Wayvgqt81tnAbSbPFC5XzRTDc5/tSCrBf/tSMAXEm1CeUznC/ZL664iH5KNwvvmPtJ8jqQ==} engines: {node: '>=20.19.0'} - '@napi-rs/wasm-runtime@1.0.7': - resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + '@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 '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -677,98 +670,220 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-resolver/binding-android-arm-eabi@11.13.2': - resolution: {integrity: sha512-vWd1NEaclg/t2DtEmYzRRBNQOueMI8tixw/fSNZ9XETXLRJiAjQMYpYeflQdRASloGze6ZelHE/wIBNt4S+pkw==} + '@oxc-parser/binding-android-arm-eabi@0.137.0': + resolution: {integrity: sha512-KDs+0VPdEmasOkpuJHW9V5WCF+cvYdMQv2Jd+aJXt+cxIx12NToRQRbXaRwUEDsZw+/jMk81Ve8ZFbjUkJTOwA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxc-resolver/binding-android-arm64@11.13.2': - resolution: {integrity: sha512-jxZrYcxgpI6IuQpguQVAQNrZfUyiYfMVqR4pKVU3PRLCM7AsfXNKp0TIgcvp+l6dYVdoZ1MMMMa5Ayjd09rNOw==} + '@oxc-parser/binding-android-arm64@0.137.0': + resolution: {integrity: sha512-WhALNzfy3x/RfC6bsqX+csavuUY0yHHE7XfgPE5M542uhoBZUUoGTPG+nkMbGoG4+gcfss5s7urMyn5QBHu0sw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxc-resolver/binding-darwin-arm64@11.13.2': - resolution: {integrity: sha512-RDS3HUe1FvgjNS1xfBUqiEJ8938Zb5r7iKABwxEblp3K4ufZZNAtoaHjdUH2TJ0THDmuf0OxxVUO/Y+4Ep4QfQ==} + '@oxc-parser/binding-darwin-arm64@0.137.0': + resolution: {integrity: sha512-bFPr5hgmNMOMoyPTGtdsK4Ug21RovIPojRMgDDhSp1LtCnc/DkLwGONKjgRjszg677RlGnkYSviQ8hHaUPOVYA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxc-resolver/binding-darwin-x64@11.13.2': - resolution: {integrity: sha512-tDcyWtkUzkt6auJLP2dOjL84BxqHkKW4mz2lNRIGPTq7b+HBraB+m8RdRH6BgqTvbnNECOxR3XAMaKBKC8J51g==} + '@oxc-parser/binding-darwin-x64@0.137.0': + resolution: {integrity: sha512-CL5dMm1asqXIDZHg14FLxj3Mc36w8PI7xCWh1uA4is6z8g2XrIILoTcQYOxDbwzuk34RDPX5IAGUxZr6LA9KAg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxc-resolver/binding-freebsd-x64@11.13.2': - resolution: {integrity: sha512-fpaeN8Q0kWvKns9uSMg6CcKo7cdgmWt6J91stPf8sdM+EKXzZ0YcRnWWyWF8SM16QcLUPCy5Iwt5Z8aYBGaZYA==} + '@oxc-parser/binding-freebsd-x64@0.137.0': + resolution: {integrity: sha512-79h8rYGnSlKPGWo7mHr2ixO6ea7aW8B0CT965SZ8SLbNnCOH5aOYBTeVXUY6eMvEaiLyWr8Skuiugr5pDYgLGw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxc-resolver/binding-linux-arm-gnueabihf@11.13.2': - resolution: {integrity: sha512-idBgJU5AvSsGOeaIWiFBKbNBjpuduHsJmrG4CBbEUNW/Ykx+ISzcuj1PHayiYX6R9stVsRhj3d2PyymfC5KWRg==} + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + resolution: {integrity: sha512-ASgmlSimhGyr0lksgVIo6hibz1obnDq4qJbiMX/AzltfgPnanRrzG1Q+23g8ljOHOjv6dsznkUuCYL3gg0sY1Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm-musleabihf@11.13.2': - resolution: {integrity: sha512-BlBvQUhvvIM/7s96KlKhMk0duR2sj8T7Hyii46/5QnwfN/pHwobvOL5czZ6/SKrHNB/F/qDY4hGsBuB1y7xgTg==} + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + resolution: {integrity: sha512-AU2J9aa22Sx32wRGnDjybOU9TQXXQUud5sdUi+ZB0XxwM8aToWLweV+yA0wlQm0yIUVqljquqoHCYEq9II8gJQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxc-resolver/binding-linux-arm64-gnu@11.13.2': - resolution: {integrity: sha512-lUmDTmYOGpbIK+FBfZ0ySaQTo7g1Ia/WnDnQR2wi/0AtehZIg/ZZIgiT/fD0iRvKEKma612/0PVo8dXdAKaAGA==} + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': + resolution: {integrity: sha512-GdEtiG89yMr7XkUGxifgodXEEm2f+xW2f9CpDjlgAnBOwhTmrpQMvhOGobLVKUyzf/qHBXW16smk5zbF3nZU6w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-arm64-musl@11.13.2': - resolution: {integrity: sha512-dkGzOxo+I9lA4Er6qzFgkFevl3JvwyI9i0T/PkOJHva04rb1p9dz8GPogTO9uMK4lrwLWzm/piAu+tHYC7v7+w==} + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + resolution: {integrity: sha512-EGJ+Bs8iXx8KBH8DQ5BLoEm5lnHaYjlh4/8j8vFhrr/6z4tqONy5BZDzLpKmmNWlN6Hlc5r8YOuBVHqZ9vRFEQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxc-resolver/binding-linux-ppc64-gnu@11.13.2': - resolution: {integrity: sha512-53kWsjLkVFnoSA7COdps38pBssN48zI8LfsOvupsmQ0/4VeMYb+0Ao9O6r52PtmFZsGB3S1Qjqbjl/Pswj1a3g==} + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + resolution: {integrity: sha512-vzFUQENy/fnbSe5DZWovq6tIBc1uhuMztanSW6rz1e9WdQE4gHwYuD7ZII6JnrJifd1R3RSoqiZbgRFlVL2tYQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-gnu@11.13.2': - resolution: {integrity: sha512-MfxN6DMpvmdCbGlheJ+ihy11oTcipqDfcEIQV9ah3FGXBRCZtBOHJpQDk8qI2Y+nCXVr3Nln7OSsOzoC4+rSYQ==} + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + resolution: {integrity: sha512-SfVI14HBQs9gtLcUD5hTt5hsNbdrqSUNg9S8muN+LhVQ5nf1WwH3hAoK6B9NKgdYgWAQSXFXGiiBedQ4r/BKuw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-riscv64-musl@11.13.2': - resolution: {integrity: sha512-WXrm4YiRU0ijqb72WHSjmfYaQZ7t6/kkQrFc4JtU+pUE4DZA/DEdxOuQEd4Q43VqmLvICTJWSaZMlCGQ4PSRUg==} + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + resolution: {integrity: sha512-e7Ppy4FCIFNQxT/ikSeIWFoQ0l+N9vgtRBtLcyZXeolTzApyVoPqEXsYPrcdM/9i0Bwk8knvYd37vaEMxHyi6g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxc-resolver/binding-linux-s390x-gnu@11.13.2': - resolution: {integrity: sha512-4pISWIlOFRUhWyvGCB3XUhtcwyvwGGhlXhHz7IXCXuGufaQtvR05trvw8U1ZnaPhsdPBkRhOMIedX11ayi5uXw==} + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + resolution: {integrity: sha512-Bho5qFwdhqsIFR7gipYEUlqvi3SRrY8sugxXig380MIaakBB1PyU9+7dBiBVScfImTNWhijUxdBwqrprGdq5WA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxc-resolver/binding-linux-x64-gnu@11.13.2': - resolution: {integrity: sha512-DVo6jS8n73yNAmCsUOOk2vBeC60j2RauDXQM8p7RDl0afsEaA2le22vD8tky7iNoM5tsxfBmE4sOJXEKgpwWRw==} + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + resolution: {integrity: sha512-36mGWtg7PyFzjJwGDkH6/F4o2nIDEoKXLPr/X/lwqklkomQwJJt1I5GJVmGhovUEmgPK5WAeAZMqlFCehwiy9Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-resolver/binding-linux-x64-musl@11.13.2': - resolution: {integrity: sha512-6WqrE+hQBFP35KdwQjWcZpldbTq6yJmuTVThISu+rY3+j6MaDp2ciLHTr1X68r2H/7ocOIl4k3NnOVIzeRJE3w==} + '@oxc-parser/binding-linux-x64-musl@0.137.0': + resolution: {integrity: sha512-/Jqx6+N7A44n2BdvUr7pXhVr2vFjs6WGH3unZRczwrfiH0H1zY0QwKQMG/dtRiTlKGDKGukznPT8lx84/oEsZg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxc-resolver/binding-wasm32-wasi@11.13.2': - resolution: {integrity: sha512-YpxvQmP2D+mNUkLQZbBjGz20g/pY8XoOBdPPoWMl9X68liFFjXxkPQTrZxWw4zzG/UkTM5z6dPRTyTePRsMcjw==} - engines: {node: '>=14.0.0'} + '@oxc-parser/binding-openharmony-arm64@0.137.0': + resolution: {integrity: sha512-9Uj0qHNNl+OgT1UTGwF7ixIXU6T1u2SbMidmgPy/h1h/fl2gRS6YpAxxY1gwHofcWjoTwkoMFd8xs5Vuj6GOFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxc-parser/binding-wasm32-wasi@0.137.0': + resolution: {integrity: sha512-gW2vfkytNGgMVADiuzdvOfw0mWG9za20F/1fCJsif5aBMAvWJTSbpIXbIe0XkOe0VENk+PadpQ7cZgUy2sUJcA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@oxc-resolver/binding-win32-arm64-msvc@11.13.2': - resolution: {integrity: sha512-1SKBw6KcCmvPBdEw1/Qdpv6eSDf23lCXTWz9VxTe6QUQ/1wR+HZR2uS4q6C8W6jnIswMTQbxpTvVwdRXl+ufeA==} + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': + resolution: {integrity: sha512-x+pFANF0yL5uK/6T7lu6SlR5qid6sp//eZXKLq5iNsIE+EQg6EaS8/wsW7E91nXXjpnPhSoMOHXShSVhGRdn8w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxc-resolver/binding-win32-ia32-msvc@11.13.2': - resolution: {integrity: sha512-KEVV7wggDucxRn3vvyHnmTCPXoCT7vWpH18UVLTygibHJvNRP2zl5lBaQcCIdIaYYZjKt1aGI/yZqxZvHoiCdg==} + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': + resolution: {integrity: sha512-sQUqym80PFi6McRsIqfJrSu2JrSClEZIXXD+/FjAFoULEKzOPsldIdFBG96xdX8aVMzCNQ9792FPx3MfkEIrFA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxc-resolver/binding-win32-x64-msvc@11.13.2': - resolution: {integrity: sha512-6AAdN9v/wO5c3td1yidgNLKYlzuNgfOtEqBq60WE469bJWR7gHgG/S5aLR2pH6/gyPLs9UXtItxi934D+0Estg==} + '@oxc-parser/binding-win32-x64-msvc@0.137.0': + resolution: {integrity: sha512-2AsevxlvNN4WKxpEn3RtqD5zbqMaXF+T7JXblsP4gVuY+vC9dXS4ED/PwfRCliFqoeisYS3Iro4DHzxr0TEvVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': + resolution: {integrity: sha512-eNU11A2WNizh04v3uyaJCootrHIaS0B9aHYXvAvVnPNk4xYSjMUjHnhQ6dewPN2MRYDskV85d1N0Aw0WNWhcyg==} + cpu: [arm] + os: [android] + + '@oxc-resolver/binding-android-arm64@11.21.3': + resolution: {integrity: sha512-8Q+ZjTLvn2dIcWsrmhdrEihm7q+ag/k+mkry7Z+t0QbbHaVxXQfvH9AewyVMh/WrpEKhQ3DDgx9fYbqeCpeOEw==} + cpu: [arm64] + os: [android] + + '@oxc-resolver/binding-darwin-arm64@11.21.3': + resolution: {integrity: sha512-wkh0qKZGHXVUDxFw3oA1TXnU2BDYY/r775oJflGeIr8uDPPoN2pk8gijQIzYRT6hoql/lg3+Tx/SaTn9e2/aGg==} + cpu: [arm64] + os: [darwin] + + '@oxc-resolver/binding-darwin-x64@11.21.3': + resolution: {integrity: sha512-HbNc23FAQYbuyDV2vBWMez4u4mrsm5RAkniGZAWqr6lYZ3N4beeqIb776jzwRl8qL2zRhHVXpUj97X0QgogVzg==} + cpu: [x64] + os: [darwin] + + '@oxc-resolver/binding-freebsd-x64@11.21.3': + resolution: {integrity: sha512-K6xNsTUPEUdfrn0+kbMq5nOUB5w1C5pavPQngt4TM2FpN91lP0PBe2srSpamb4d69O7h86oAi/qWX/kZNRSjkw==} + cpu: [x64] + os: [freebsd] + + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': + resolution: {integrity: sha512-VcFmOpcpWX1zoEy8M58tR2M9YxM+Z9RuQhqAx5q0CTmrruaP7Gveejg75hzd/5sg5nk9G3aLALEa3hE2FsmmTQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': + resolution: {integrity: sha512-quVoxFLBy43hWaQbbDtQNRwAX5vX76mv7n64icAtQcJ3eNgVeblqmkupF/hAneNthdqSlnd1sTjb3aQSaDPaCQ==} + cpu: [arm] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': + resolution: {integrity: sha512-X0AqNZgcD07Q4V3RDK18/vYOj/HQT/FnmEFGYS2jTWqY7JO13ryE3TEs3eAIgUJhBnNkpEaiXqz3VK8M7qQhWQ==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': + resolution: {integrity: sha512-YkaQnaKYdbuaXvRt5Qd0GpbihzVnyfR6z1SpYfIUC6RTu4NF7lDKPjVkYb+jRI2gedVO2rVpN35Y6akG6ud4Lw==} + cpu: [arm64] + os: [linux] + + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': + resolution: {integrity: sha512-gB9HwhrPiFqUzDeEq+y/CgAijz1YdI6BnXz5GaH2Pa9cWdutchlkGFAiAuGb/PjVQpiK6NFKzFuztxrweoit7A==} + cpu: [ppc64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': + resolution: {integrity: sha512-zjDWBlYk8QGv0H8dsPUWqkfjYIIjG2TvspGkzXL0eImbgxtZorA/klKeHyolevoT3Kvbi+1iMr9Lhrh7jf54Og==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': + resolution: {integrity: sha512-4UfsQvacV388y1zpXL7C1x1FNYaV52JtuNRiuzrfQA2z1z6ElVrsidkGsrvQ5EgeSq1Pj7kaKqrgGkvFuxJ/tw==} + cpu: [riscv64] + os: [linux] + + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': + resolution: {integrity: sha512-b5uH+HKH0MP5mNBYaK75SKsJbw52URqrx2LavYdq6wb0l3ExAG5niYRP9DWUNHdKilpaBVM2bXk9HNWrH3ew7Q==} + cpu: [s390x] + os: [linux] + + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': + resolution: {integrity: sha512-PjYlmilBpNRh2ntXNYAK3Am5w/nPfEpnU/96iNx7CI8EzAn12J4JRiec63wHJTH31nLoCNxBg/829pN+3CfG3Q==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-linux-x64-musl@11.21.3': + resolution: {integrity: sha512-QTBAb7JuHlZ7JUEyM8UiQi2f7m/L4swBhP2TNpYIDc9Wp/wRw1G/8sl6i13aIzQAXH7LKIm294LeOHd0lQR8zA==} + cpu: [x64] + os: [linux] + + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + resolution: {integrity: sha512-4j1DFwjwv36ec9kds0jU/ucQ5Ha4ERO/H95BxR5JFf0kqUUAJ1kwII7XhTc1vZrkdJkvLGC9Q2MbpObpum8RBg==} + cpu: [arm64] + os: [openharmony] + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + resolution: {integrity: sha512-i8oluoel5kru/j1WNrjmQSiA3GQ7wvIYVR1IwIoZtKogAhya2iub+ZKIeSIkcJOrnzQ18Tzl/F+kL3fYOxZLvA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + resolution: {integrity: sha512-M/8dw8dD6aOs+NlPJax401CZB9I7Aut84isQLgALGGwke4Afvw+/7yYhZb94yXf6t2sPLhQLmSmtSV+2FhsOWg==} + cpu: [arm64] + os: [win32] + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': + resolution: {integrity: sha512-H7BCt/VnS9hnmMp42eGhZ99izSCRvlnWwy/N71K1/J8QoExwY4262Z8QiEkMDtduRJrztayDxETTckmUuAVL9Q==} cpu: [x64] os: [win32] @@ -900,8 +1015,8 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -912,17 +1027,11 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@24.10.1': - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - - '@types/node@25.3.0': - resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} + '@types/node@26.1.1': + resolution: {integrity: sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==} '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} @@ -930,64 +1039,125 @@ packages: '@types/signale@1.4.7': resolution: {integrity: sha512-nc0j37QupTT7OcYeH3gRE1ZfzUalEUsDKJsJ3IsJr0pjjFZTjtrX1Bsn6Kv56YXI/H9rNSwAkIPRxNlZI8GyQw==} - '@typescript-eslint/eslint-plugin@8.46.4': - resolution: {integrity: sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - '@typescript-eslint/parser': ^8.46.4 - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] - '@typescript-eslint/parser@8.46.4': - resolution: {integrity: sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] - '@typescript-eslint/project-service@8.46.4': - resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] - '@typescript-eslint/scope-manager@8.46.4': - resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] - '@typescript-eslint/tsconfig-utils@8.46.4': - resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] - '@typescript-eslint/type-utils@8.46.4': - resolution: {integrity: sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] - '@typescript-eslint/types@8.46.4': - resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] - '@typescript-eslint/typescript-estree@8.46.4': - resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] - '@typescript-eslint/utils@8.46.4': - resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] - '@typescript-eslint/visitor-keys@8.46.4': - resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] '@vitest/coverage-v8@4.0.18': resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} @@ -1031,16 +1201,6 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -1054,9 +1214,6 @@ packages: ajv: optional: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} @@ -1072,10 +1229,6 @@ packages: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1096,9 +1249,6 @@ packages: ast-v8-to-istanbul@0.3.11: resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.3: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} @@ -1111,12 +1261,6 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.2: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} @@ -1147,10 +1291,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1159,10 +1299,6 @@ packages: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} @@ -1173,23 +1309,12 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1198,9 +1323,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -1241,9 +1363,6 @@ packages: supports-color: optional: true - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1306,66 +1425,14 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-config-prettier@10.1.8: - resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.1: - resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - eta@4.5.1: resolution: {integrity: sha512-EaNCGm+8XEIU7YNcc+THptWAO5NfKBHHARxt+wxZljj9bTr/+arRoOm9/MpGt4n6xn9fLnPFRSoLD0WFYGFUxQ==} engines: {node: '>=20'} @@ -1406,12 +1473,6 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -1434,10 +1495,6 @@ packages: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1454,20 +1511,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - formatly@0.3.0: resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} engines: {node: '>=18.3.0'} @@ -1505,8 +1551,8 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} giget@3.1.2: resolution: {integrity: sha512-T2qUpKBHeUTwHcIhydgnJzhL0Hj785ms+JkxaaWQH9SDM/llXeewnOkfJcFShAHjWI+26hOChwUfCoupaXLm8g==} @@ -1516,19 +1562,11 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} hasBin: true - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1540,9 +1578,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1586,18 +1621,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1650,8 +1673,8 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jose@6.1.3: @@ -1668,45 +1691,26 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-better-errors@1.0.2: resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} json-schema-typed@8.0.2: resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - knip@5.69.1: - resolution: {integrity: sha512-duK61dmrk9O3RiPQtDrsWGzxGyNoyf9gQyjXPcvmcecZypdv9x0ixzPdBU4YTPRbwvUaRtNGulHYUyixz2fbKg==} - engines: {node: '>=18.18.0'} + knip@6.26.0: + resolution: {integrity: sha512-e9eELEEpBpGTd4H4HB7818/DYj9dMzMyUqAddfYwUN/EbSkgIjOuWEF96W/xHsmV0SDrsdXjIM+oZ2xpPzPsBA==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - peerDependencies: - '@types/node': '>=18' - typescript: '>=5.0.4 <7' - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} @@ -1735,16 +1739,9 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -1794,16 +1791,6 @@ packages: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -1826,9 +1813,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -1851,15 +1835,15 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - oxc-resolver@11.13.2: - resolution: {integrity: sha512-1SXVyYQ9bqMX3uZo8Px81EG7jhZkO9PvvR5X9roY5TLYVm4ZA7pbPDNlYaDBBeF9U+YO3OeMNoHde52hrcCu8w==} + oxc-parser@0.137.0: + resolution: {integrity: sha512-yFImD+WLElJpLKy8llG1qe4DCmMsL18peRp8XP1JKfig/gISbJkglnpDtX2aTmAn10kZF7164HbN2H8QPsXxGg==} + engines: {node: ^20.19.0 || >=22.12.0} + + oxc-resolver@11.21.3: + resolution: {integrity: sha512-2Mx3fKQz7+xgrBONjsxOgCGtMHOn38/HxMzW1I5efwXB5a4lRN0Vp40gYUJFBWJslcrvwoofTrqoTnLbwTd3pA==} p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} @@ -1873,10 +1857,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@2.0.0: resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} engines: {node: '>=4'} @@ -1885,10 +1865,6 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - p-map@2.1.0: resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} engines: {node: '>=6'} @@ -1907,10 +1883,6 @@ packages: package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - parse-json@4.0.0: resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} engines: {node: '>=4'} @@ -1948,14 +1920,18 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pify@3.0.0: resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} engines: {node: '>=4'} @@ -2001,28 +1977,15 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} @@ -2053,10 +2016,6 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -2143,8 +2102,8 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - smol-toml@1.5.2: - resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} + smol-toml@1.7.0: + resolution: {integrity: sha512-aqVvWoyO21L23mb+drl4RmMXbf6N7FdHjAhTRA9ZBL7apWBgfWC16KjrASI+1p9GAroljyMHj6fK67i0UiTNvQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -2179,12 +2138,8 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - strip-json-comments@5.0.2: - resolution: {integrity: sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g==} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} engines: {node: '>=14.16'} sucrase@3.35.1: @@ -2225,6 +2180,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} @@ -2241,12 +2200,6 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -2277,10 +2230,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -2290,14 +2239,20 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + unbash@4.0.2: + resolution: {integrity: sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==} + engines: {node: '>=14'} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -2307,9 +2262,6 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2402,16 +2354,13 @@ packages: engines: {node: '>=8'} hasBin: true - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} @@ -2421,6 +2370,9 @@ packages: zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: '@babel/helper-string-parser@7.27.1': {} @@ -2440,9 +2392,44 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@changesets/apply-release-plan@7.0.13': + '@biomejs/biome@2.5.3': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.5.3 + '@biomejs/cli-darwin-x64': 2.5.3 + '@biomejs/cli-linux-arm64': 2.5.3 + '@biomejs/cli-linux-arm64-musl': 2.5.3 + '@biomejs/cli-linux-x64': 2.5.3 + '@biomejs/cli-linux-x64-musl': 2.5.3 + '@biomejs/cli-win32-arm64': 2.5.3 + '@biomejs/cli-win32-x64': 2.5.3 + + '@biomejs/cli-darwin-arm64@2.5.3': + optional: true + + '@biomejs/cli-darwin-x64@2.5.3': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.5.3': + optional: true + + '@biomejs/cli-linux-arm64@2.5.3': + optional: true + + '@biomejs/cli-linux-x64-musl@2.5.3': + optional: true + + '@biomejs/cli-linux-x64@2.5.3': + optional: true + + '@biomejs/cli-win32-arm64@2.5.3': + optional: true + + '@biomejs/cli-win32-x64@2.5.3': + optional: true + + '@changesets/apply-release-plan@7.1.1': dependencies: - '@changesets/config': 3.1.1 + '@changesets/config': 3.1.4 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.4 '@changesets/should-skip-package': 0.1.2 @@ -2456,10 +2443,10 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.4 - '@changesets/assemble-release-plan@6.0.9': + '@changesets/assemble-release-plan@6.0.10': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-dependents-graph': 2.1.4 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -2469,30 +2456,28 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.7(@types/node@24.10.1)': + '@changesets/cli@2.31.0(@types/node@26.1.1)': dependencies: - '@changesets/apply-release-plan': 7.0.13 - '@changesets/assemble-release-plan': 6.0.9 + '@changesets/apply-release-plan': 7.1.1 + '@changesets/assemble-release-plan': 6.0.10 '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.1 + '@changesets/config': 3.1.4 '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.13 + '@changesets/get-dependents-graph': 2.1.4 + '@changesets/get-release-plan': 4.0.16 '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.5 + '@changesets/read': 0.6.7 '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@24.10.1) + '@inquirer/external-editor': 1.0.3(@types/node@26.1.1) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 - ci-info: 3.9.0 enquirer: 2.4.1 fs-extra: 7.0.1 mri: 1.2.0 - p-limit: 2.3.0 package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 @@ -2502,11 +2487,12 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@changesets/config@3.1.1': + '@changesets/config@3.1.4': dependencies: '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-dependents-graph': 2.1.4 '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 @@ -2516,19 +2502,19 @@ snapshots: dependencies: extendable-error: 0.1.7 - '@changesets/get-dependents-graph@2.1.3': + '@changesets/get-dependents-graph@2.1.4': dependencies: '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 semver: 7.7.4 - '@changesets/get-release-plan@4.0.13': + '@changesets/get-release-plan@4.0.16': dependencies: - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.1 + '@changesets/assemble-release-plan': 6.0.10 + '@changesets/config': 3.1.4 '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.5 + '@changesets/read': 0.6.7 '@changesets/types': 6.1.0 '@manypkg/get-packages': 1.1.3 @@ -2546,10 +2532,10 @@ snapshots: dependencies: picocolors: 1.1.1 - '@changesets/parse@0.4.1': + '@changesets/parse@0.4.3': dependencies: '@changesets/types': 6.1.0 - js-yaml: 3.14.2 + js-yaml: 4.3.0 '@changesets/pre@2.0.2': dependencies: @@ -2558,11 +2544,11 @@ snapshots: '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.5': + '@changesets/read@0.6.7': dependencies: '@changesets/git': 3.0.4 '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.1 + '@changesets/parse': 0.4.3 '@changesets/types': 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 @@ -2605,18 +2591,29 @@ snapshots: dependencies: commander: 14.0.3 - '@emnapi/core@1.7.1': + '@emnapi/core@1.11.0': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.0': dependencies: - '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.1': + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true @@ -2777,73 +2774,16 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': - dependencies: - eslint: 9.39.1(jiti@2.6.1) - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.1': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.1': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - '@hono/node-server@1.19.9(hono@4.12.0)': dependencies: hono: 4.12.0 - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@inquirer/external-editor@1.0.3(@types/node@24.10.1)': + '@inquirer/external-editor@1.0.3(@types/node@26.1.1)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.0 optionalDependencies: - '@types/node': 24.10.1 + '@types/node': 26.1.1 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -2897,87 +2837,162 @@ snapshots: transitivePeerDependencies: - supports-color - '@mrleebo/prisma-ast@0.14.0': - dependencies: - chevrotain: 11.1.1 - lilconfig: 2.1.0 + '@mrleebo/prisma-ast@0.14.0': + dependencies: + chevrotain: 11.1.1 + lilconfig: 2.1.0 + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@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 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@oxc-parser/binding-android-arm-eabi@0.137.0': + optional: true + + '@oxc-parser/binding-android-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-darwin-x64@0.137.0': + optional: true + + '@oxc-parser/binding-freebsd-x64@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-gnueabihf@0.137.0': + optional: true + + '@oxc-parser/binding-linux-arm-musleabihf@0.137.0': + optional: true - '@napi-rs/wasm-runtime@1.0.7': - dependencies: - '@emnapi/core': 1.7.1 - '@emnapi/runtime': 1.7.1 - '@tybys/wasm-util': 0.10.1 + '@oxc-parser/binding-linux-arm64-gnu@0.137.0': optional: true - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@oxc-parser/binding-linux-arm64-musl@0.137.0': + optional: true - '@nodelib/fs.stat@2.0.5': {} + '@oxc-parser/binding-linux-ppc64-gnu@0.137.0': + optional: true - '@nodelib/fs.walk@1.2.8': + '@oxc-parser/binding-linux-riscv64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-riscv64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-linux-s390x-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-gnu@0.137.0': + optional: true + + '@oxc-parser/binding-linux-x64-musl@0.137.0': + optional: true + + '@oxc-parser/binding-openharmony-arm64@0.137.0': + optional: true + + '@oxc-parser/binding-wasm32-wasi@0.137.0': dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 + '@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) + optional: true - '@oxc-resolver/binding-android-arm-eabi@11.13.2': + '@oxc-parser/binding-win32-arm64-msvc@0.137.0': optional: true - '@oxc-resolver/binding-android-arm64@11.13.2': + '@oxc-parser/binding-win32-ia32-msvc@0.137.0': optional: true - '@oxc-resolver/binding-darwin-arm64@11.13.2': + '@oxc-parser/binding-win32-x64-msvc@0.137.0': optional: true - '@oxc-resolver/binding-darwin-x64@11.13.2': + '@oxc-project/types@0.137.0': {} + + '@oxc-resolver/binding-android-arm-eabi@11.21.3': optional: true - '@oxc-resolver/binding-freebsd-x64@11.13.2': + '@oxc-resolver/binding-android-arm64@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm-gnueabihf@11.13.2': + '@oxc-resolver/binding-darwin-arm64@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm-musleabihf@11.13.2': + '@oxc-resolver/binding-darwin-x64@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm64-gnu@11.13.2': + '@oxc-resolver/binding-freebsd-x64@11.21.3': optional: true - '@oxc-resolver/binding-linux-arm64-musl@11.13.2': + '@oxc-resolver/binding-linux-arm-gnueabihf@11.21.3': optional: true - '@oxc-resolver/binding-linux-ppc64-gnu@11.13.2': + '@oxc-resolver/binding-linux-arm-musleabihf@11.21.3': optional: true - '@oxc-resolver/binding-linux-riscv64-gnu@11.13.2': + '@oxc-resolver/binding-linux-arm64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-riscv64-musl@11.13.2': + '@oxc-resolver/binding-linux-arm64-musl@11.21.3': optional: true - '@oxc-resolver/binding-linux-s390x-gnu@11.13.2': + '@oxc-resolver/binding-linux-ppc64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-x64-gnu@11.13.2': + '@oxc-resolver/binding-linux-riscv64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-linux-x64-musl@11.13.2': + '@oxc-resolver/binding-linux-riscv64-musl@11.21.3': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.13.2': - dependencies: - '@napi-rs/wasm-runtime': 1.0.7 + '@oxc-resolver/binding-linux-s390x-gnu@11.21.3': optional: true - '@oxc-resolver/binding-win32-arm64-msvc@11.13.2': + '@oxc-resolver/binding-linux-x64-gnu@11.21.3': optional: true - '@oxc-resolver/binding-win32-ia32-msvc@11.13.2': + '@oxc-resolver/binding-linux-x64-musl@11.21.3': optional: true - '@oxc-resolver/binding-win32-x64-msvc@11.13.2': + '@oxc-resolver/binding-openharmony-arm64@11.21.3': + optional: true + + '@oxc-resolver/binding-wasm32-wasi@11.21.3': + dependencies: + '@emnapi/core': 1.11.0 + '@emnapi/runtime': 1.11.0 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0) + optional: true + + '@oxc-resolver/binding-win32-arm64-msvc@11.21.3': + optional: true + + '@oxc-resolver/binding-win32-x64-msvc@11.21.3': optional: true '@rollup/rollup-android-arm-eabi@4.57.1': @@ -3057,7 +3072,7 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@tybys/wasm-util@0.10.1': + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 optional: true @@ -3071,118 +3086,79 @@ snapshots: '@types/estree@1.0.8': {} - '@types/json-schema@7.0.15': {} - '@types/node@12.20.55': {} - '@types/node@24.10.1': + '@types/node@26.1.1': dependencies: - undici-types: 7.16.0 - - '@types/node@25.3.0': - dependencies: - undici-types: 7.18.2 + undici-types: 8.3.0 '@types/semver@7.7.1': {} '@types/signale@1.4.7': dependencies: - '@types/node': 25.3.0 - - '@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/type-utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.4 - eslint: 9.39.1(jiti@2.6.1) - graphemer: 1.4.0 - ignore: 7.0.5 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@types/node': 26.1.1 - '@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.46.4 - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true - '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - debug: 4.4.3 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true - '@typescript-eslint/scope-manager@8.46.4': - dependencies: - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/visitor-keys': 8.46.4 + '@typescript/typescript-darwin-x64@7.0.2': + optional: true - '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true - '@typescript-eslint/type-utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true - '@typescript-eslint/types@8.46.4': {} + '@typescript/typescript-linux-arm64@7.0.2': + optional: true - '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/visitor-keys': 8.46.4 - debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.4 - ts-api-utils: 2.1.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript/typescript-linux-arm@7.0.2': + optional: true - '@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.4 - '@typescript-eslint/types': 8.46.4 - '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) - eslint: 9.39.1(jiti@2.6.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color + '@typescript/typescript-linux-loong64@7.0.2': + optional: true - '@typescript-eslint/visitor-keys@8.46.4': - dependencies: - '@typescript-eslint/types': 8.46.4 - eslint-visitor-keys: 4.2.1 + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true - '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6))': + '@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.0.18 @@ -3194,7 +3170,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6) + vitest: 4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0) '@vitest/expect@4.0.18': dependencies: @@ -3205,13 +3181,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6))': + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6) + vite: 7.3.1(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0) '@vitest/pretty-format@4.0.18': dependencies: @@ -3240,25 +3216,12 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - - acorn@8.15.0: {} - acorn@8.16.0: {} ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 @@ -3274,10 +3237,6 @@ snapshots: dependencies: color-convert: 1.9.3 - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - any-promise@1.3.0: {} argparse@1.0.10: @@ -3296,8 +3255,6 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - balanced-match@1.0.2: {} - balanced-match@4.0.3: {} better-path-resolve@1.0.0: @@ -3318,15 +3275,6 @@ snapshots: transitivePeerDependencies: - supports-color - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - brace-expansion@5.0.2: dependencies: balanced-match: 4.0.3 @@ -3354,8 +3302,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - callsites@3.1.0: {} - chai@6.2.2: {} chalk@2.4.2: @@ -3364,11 +3310,6 @@ snapshots: escape-string-regexp: 1.0.5 supports-color: 5.5.0 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chardet@2.1.1: {} chevrotain@11.1.1: @@ -3384,26 +3325,16 @@ snapshots: dependencies: readdirp: 4.1.2 - ci-info@3.9.0: {} - color-convert@1.9.3: dependencies: color-name: 1.1.3 - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} - commander@14.0.3: {} commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} consola@3.4.2: {} @@ -3431,8 +3362,6 @@ snapshots: dependencies: ms: 2.1.3 - deep-is@0.1.4: {} - depd@2.0.0: {} detect-indent@6.1.0: {} @@ -3533,86 +3462,12 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} - - eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)): - dependencies: - eslint: 9.39.1(jiti@2.6.1) - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.1(jiti@2.6.1): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.1 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 2.6.1 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - esprima@4.0.1: {} - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 - esutils@2.0.3: {} - eta@4.5.1: {} etag@1.8.1: {} @@ -3675,10 +3530,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - fast-uri@3.1.0: {} fastq@1.19.1: @@ -3693,14 +3544,14 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + figures@2.0.0: dependencies: escape-string-regexp: 1.0.5 - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3725,24 +3576,12 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.0 rollup: 4.57.1 - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - formatly@0.3.0: dependencies: fd-package-json: 2.0.0 @@ -3786,10 +3625,9 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-tsconfig@4.13.6: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 - optional: true giget@3.1.2: {} @@ -3797,18 +3635,12 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - glob@13.0.6: dependencies: minimatch: 10.2.2 minipass: 7.1.3 path-scurry: 2.0.2 - globals@14.0.0: {} - globby@11.1.0: dependencies: array-union: 2.1.0 @@ -3822,8 +3654,6 @@ snapshots: graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -3858,15 +3688,6 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - inherits@2.0.4: {} ip-address@10.0.1: {} @@ -3906,7 +3727,7 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jiti@2.6.1: {} + jiti@2.7.0: {} jose@6.1.3: {} @@ -3919,51 +3740,35 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.1: + js-yaml@4.3.0: dependencies: argparse: 2.0.1 - json-buffer@3.0.1: {} - json-parse-better-errors@1.0.2: {} - json-schema-traverse@0.4.1: {} - json-schema-traverse@1.0.0: {} json-schema-typed@8.0.2: {} - json-stable-stringify-without-jsonify@1.0.1: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - knip@5.69.1(@types/node@24.10.1)(typescript@5.9.3): + knip@6.26.0: dependencies: - '@nodelib/fs.walk': 1.2.8 - '@types/node': 24.10.1 - fast-glob: 3.3.3 + fdir: 6.5.0(picomatch@4.0.5) formatly: 0.3.0 - jiti: 2.6.1 - js-yaml: 4.1.1 - minimist: 1.2.8 - oxc-resolver: 11.13.2 - picocolors: 1.1.1 - picomatch: 4.0.3 - smol-toml: 1.5.2 - strip-json-comments: 5.0.2 - typescript: 5.9.3 - zod: 4.3.6 - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 + get-tsconfig: 4.14.0 + jiti: 2.7.0 + oxc-parser: 0.137.0 + oxc-resolver: 11.21.3 + picomatch: 4.0.5 + smol-toml: 1.7.0 + strip-json-comments: 5.0.3 + tinyglobby: 0.2.17 + unbash: 4.0.2 + yaml: 2.9.0 + zod: 4.4.3 lilconfig@2.1.0: {} @@ -3989,14 +3794,8 @@ snapshots: dependencies: p-locate: 4.1.0 - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - lodash-es@4.17.23: {} - lodash.merge@4.6.2: {} - lodash.startcase@4.4.0: {} lru-cache@11.2.6: {} @@ -4026,7 +3825,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.54.0: {} @@ -4038,16 +3837,6 @@ snapshots: dependencies: brace-expansion: 5.0.2 - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.2 - - minimist@1.2.8: {} - minipass@7.1.3: {} mlly@1.8.0: @@ -4069,8 +3858,6 @@ snapshots: nanoid@3.3.11: {} - natural-compare@1.4.0: {} - negotiator@1.0.0: {} object-assign@4.1.1: {} @@ -4087,38 +3874,54 @@ snapshots: dependencies: wrappy: 1.0.2 - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - outdent@0.5.0: {} - oxc-resolver@11.13.2: + oxc-parser@0.137.0: + dependencies: + '@oxc-project/types': 0.137.0 + optionalDependencies: + '@oxc-parser/binding-android-arm-eabi': 0.137.0 + '@oxc-parser/binding-android-arm64': 0.137.0 + '@oxc-parser/binding-darwin-arm64': 0.137.0 + '@oxc-parser/binding-darwin-x64': 0.137.0 + '@oxc-parser/binding-freebsd-x64': 0.137.0 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.137.0 + '@oxc-parser/binding-linux-arm-musleabihf': 0.137.0 + '@oxc-parser/binding-linux-arm64-gnu': 0.137.0 + '@oxc-parser/binding-linux-arm64-musl': 0.137.0 + '@oxc-parser/binding-linux-ppc64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-gnu': 0.137.0 + '@oxc-parser/binding-linux-riscv64-musl': 0.137.0 + '@oxc-parser/binding-linux-s390x-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-gnu': 0.137.0 + '@oxc-parser/binding-linux-x64-musl': 0.137.0 + '@oxc-parser/binding-openharmony-arm64': 0.137.0 + '@oxc-parser/binding-wasm32-wasi': 0.137.0 + '@oxc-parser/binding-win32-arm64-msvc': 0.137.0 + '@oxc-parser/binding-win32-ia32-msvc': 0.137.0 + '@oxc-parser/binding-win32-x64-msvc': 0.137.0 + + oxc-resolver@11.21.3: optionalDependencies: - '@oxc-resolver/binding-android-arm-eabi': 11.13.2 - '@oxc-resolver/binding-android-arm64': 11.13.2 - '@oxc-resolver/binding-darwin-arm64': 11.13.2 - '@oxc-resolver/binding-darwin-x64': 11.13.2 - '@oxc-resolver/binding-freebsd-x64': 11.13.2 - '@oxc-resolver/binding-linux-arm-gnueabihf': 11.13.2 - '@oxc-resolver/binding-linux-arm-musleabihf': 11.13.2 - '@oxc-resolver/binding-linux-arm64-gnu': 11.13.2 - '@oxc-resolver/binding-linux-arm64-musl': 11.13.2 - '@oxc-resolver/binding-linux-ppc64-gnu': 11.13.2 - '@oxc-resolver/binding-linux-riscv64-gnu': 11.13.2 - '@oxc-resolver/binding-linux-riscv64-musl': 11.13.2 - '@oxc-resolver/binding-linux-s390x-gnu': 11.13.2 - '@oxc-resolver/binding-linux-x64-gnu': 11.13.2 - '@oxc-resolver/binding-linux-x64-musl': 11.13.2 - '@oxc-resolver/binding-wasm32-wasi': 11.13.2 - '@oxc-resolver/binding-win32-arm64-msvc': 11.13.2 - '@oxc-resolver/binding-win32-ia32-msvc': 11.13.2 - '@oxc-resolver/binding-win32-x64-msvc': 11.13.2 + '@oxc-resolver/binding-android-arm-eabi': 11.21.3 + '@oxc-resolver/binding-android-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-arm64': 11.21.3 + '@oxc-resolver/binding-darwin-x64': 11.21.3 + '@oxc-resolver/binding-freebsd-x64': 11.21.3 + '@oxc-resolver/binding-linux-arm-gnueabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm-musleabihf': 11.21.3 + '@oxc-resolver/binding-linux-arm64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-arm64-musl': 11.21.3 + '@oxc-resolver/binding-linux-ppc64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-riscv64-musl': 11.21.3 + '@oxc-resolver/binding-linux-s390x-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-gnu': 11.21.3 + '@oxc-resolver/binding-linux-x64-musl': 11.21.3 + '@oxc-resolver/binding-openharmony-arm64': 11.21.3 + '@oxc-resolver/binding-wasm32-wasi': 11.21.3 + '@oxc-resolver/binding-win32-arm64-msvc': 11.21.3 + '@oxc-resolver/binding-win32-x64-msvc': 11.21.3 p-filter@2.1.0: dependencies: @@ -4132,10 +3935,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@2.0.0: dependencies: p-limit: 1.3.0 @@ -4144,10 +3943,6 @@ snapshots: dependencies: p-limit: 2.3.0 - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - p-map@2.1.0: {} p-try@1.0.0: {} @@ -4160,10 +3955,6 @@ snapshots: dependencies: quansync: 0.2.11 - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - parse-json@4.0.0: dependencies: error-ex: 1.3.4 @@ -4190,10 +3981,12 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} picomatch@4.0.3: {} + picomatch@4.0.5: {} + pify@3.0.0: {} pify@4.0.1: {} @@ -4213,13 +4006,14 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6): + postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: - jiti: 2.6.1 + jiti: 2.7.0 postcss: 8.5.6 tsx: 4.20.6 + yaml: 2.9.0 postcss@8.5.6: dependencies: @@ -4227,19 +4021,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prelude-ls@1.2.1: {} - prettier@2.8.8: {} - prettier@3.6.2: {} - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - punycode@2.3.1: {} - qs@6.15.0: dependencies: side-channel: 1.1.0 @@ -4268,12 +4056,9 @@ snapshots: require-from-string@2.0.2: {} - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: - optional: true + resolve-pkg-maps@1.0.0: {} reusify@1.1.0: {} @@ -4404,7 +4189,7 @@ snapshots: slash@3.0.0: {} - smol-toml@1.5.2: {} + smol-toml@1.7.0: {} source-map-js@1.2.1: {} @@ -4429,9 +4214,7 @@ snapshots: strip-bom@3.0.0: {} - strip-json-comments@3.1.1: {} - - strip-json-comments@5.0.2: {} + strip-json-comments@5.0.3: {} sucrase@3.35.1: dependencies: @@ -4472,6 +4255,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + tinyrainbow@3.0.3: {} to-regex-range@5.0.1: @@ -4482,16 +4270,12 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@2.1.0(typescript@5.9.3): - dependencies: - typescript: 5.9.3 - ts-interface-checker@0.1.13: {} tslib@2.8.1: optional: true - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3): + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -4502,7 +4286,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6) + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.9.0) resolve-from: 5.0.0 rollup: 4.57.1 source-map: 0.7.6 @@ -4519,18 +4303,42 @@ snapshots: - tsx - yaml + tsup@8.5.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(typescript@7.0.2)(yaml@2.9.0): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.3) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.3 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.9.0) + resolve-from: 5.0.0 + rollup: 4.57.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 7.0.2 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.20.6: dependencies: esbuild: 0.25.12 - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.0 optionalDependencies: fsevents: 2.3.3 optional: true - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -4539,23 +4347,42 @@ snapshots: typescript@5.9.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + ufo@1.6.3: {} - undici-types@7.16.0: {} + unbash@4.0.2: {} - undici-types@7.18.2: {} + undici-types@8.3.0: {} universalify@0.1.2: {} unpipe@1.0.0: {} - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - vary@1.1.2: {} - vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6): + vite@7.3.1(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -4564,15 +4391,16 @@ snapshots: rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.3.0 + '@types/node': 26.1.1 fsevents: 2.3.3 - jiti: 2.6.1 + jiti: 2.7.0 tsx: 4.20.6 + yaml: 2.9.0 - vitest@4.0.18(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6): + vitest@4.0.18(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6)) + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -4589,10 +4417,10 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(tsx@4.20.6) + vite: 7.3.1(@types/node@26.1.1)(jiti@2.7.0)(tsx@4.20.6)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.0 + '@types/node': 26.1.1 transitivePeerDependencies: - jiti - less @@ -4617,14 +4445,14 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - word-wrap@1.2.5: {} - wrappy@1.0.2: {} - yocto-queue@0.1.0: {} + yaml@2.9.0: {} zod-to-json-schema@3.25.1(zod@4.3.6): dependencies: zod: 4.3.6 zod@4.3.6: {} + + zod@4.4.3: {} diff --git a/tsconfig.base.json b/tsconfig.base.json index bb59a317..5ec5cb63 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,7 +15,8 @@ "outDir": "./dist", "resolveJsonModule": true, "isolatedModules": true, - "verbatimModuleSyntax": false + "verbatimModuleSyntax": false, + "types": ["node"] }, "exclude": [ "node_modules",