diff --git a/.changeset/bump-federation-alpha-6.md b/.changeset/bump-federation-alpha-6.md new file mode 100644 index 000000000..4cb52eb3b --- /dev/null +++ b/.changeset/bump-federation-alpha-6.md @@ -0,0 +1,5 @@ +--- +"@sanity/cli": patch +--- + +bump @sanity/federation to 0.1.0-alpha.6 diff --git a/.changeset/clear-dragons-search.md b/.changeset/clear-dragons-search.md new file mode 100644 index 000000000..b088bbb4a --- /dev/null +++ b/.changeset/clear-dragons-search.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli-core': minor +--- + +Add federation to CLI config diff --git a/.changeset/fancy-cycles-strive.md b/.changeset/fancy-cycles-strive.md new file mode 100644 index 000000000..c919660c6 --- /dev/null +++ b/.changeset/fancy-cycles-strive.md @@ -0,0 +1,5 @@ +--- +"@sanity/cli": patch +--- + +externalize sanity and @sanity/workbench diff --git a/.changeset/pr-1000.md b/.changeset/pr-1000.md new file mode 100644 index 000000000..31f90bd6d --- /dev/null +++ b/.changeset/pr-1000.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +do not resolve dist tags diff --git a/.changeset/pr-1005.md b/.changeset/pr-1005.md new file mode 100644 index 000000000..21e7f5bd0 --- /dev/null +++ b/.changeset/pr-1005.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +apply user vite config to federation builds [SDK-1281] diff --git a/.changeset/pr-1022.md b/.changeset/pr-1022.md new file mode 100644 index 000000000..637b339dc --- /dev/null +++ b/.changeset/pr-1022.md @@ -0,0 +1,7 @@ + +--- +'@sanity/cli-core': patch +'@sanity/cli': patch +--- + +watch for changes of the cli config file diff --git a/.changeset/pr-1027.md b/.changeset/pr-1027.md new file mode 100644 index 000000000..5a13a2bc6 --- /dev/null +++ b/.changeset/pr-1027.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +forward CLI project id for local applications diff --git a/.changeset/pr-1028.md b/.changeset/pr-1028.md new file mode 100644 index 000000000..c6a719f3d --- /dev/null +++ b/.changeset/pr-1028.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +fix(workbench): add `__mf__temp` directory to .gitignore diff --git a/.changeset/pr-1042.md b/.changeset/pr-1042.md new file mode 100644 index 000000000..54b2af845 --- /dev/null +++ b/.changeset/pr-1042.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +perf(workbench): preload workbench and warmup dev-server files diff --git a/.changeset/pr-1047.md b/.changeset/pr-1047.md new file mode 100644 index 000000000..0cf74d99b --- /dev/null +++ b/.changeset/pr-1047.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +fix(workbench): remove warmup for dependencies diff --git a/.changeset/pr-1057.md b/.changeset/pr-1057.md new file mode 100644 index 000000000..636e930d3 --- /dev/null +++ b/.changeset/pr-1057.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +fix(workbench): prune stale lock files diff --git a/.changeset/pr-1066.md b/.changeset/pr-1066.md new file mode 100644 index 000000000..b779df823 --- /dev/null +++ b/.changeset/pr-1066.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +fix(workbench): throw error on invalid `SANITY_INTERNAL_WORKBENCH_REMOTE_URL` diff --git a/.changeset/pr-1067.md b/.changeset/pr-1067.md new file mode 100644 index 000000000..592596d76 --- /dev/null +++ b/.changeset/pr-1067.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +fix(workbench): detect PID reuse on Windows via PowerShell diff --git a/.changeset/pr-770.md b/.changeset/pr-770.md new file mode 100644 index 000000000..34ec8f2ca --- /dev/null +++ b/.changeset/pr-770.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli': minor +--- + +Enforce single workbench instance with dev-server registry, fix PID-reuse detection and signal cleanup diff --git a/.changeset/pr-905.md b/.changeset/pr-905.md new file mode 100644 index 000000000..c4023a08c --- /dev/null +++ b/.changeset/pr-905.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli': minor +--- + +Support passing organization ID to the workbench dev server diff --git a/.changeset/pr-930.md b/.changeset/pr-930.md new file mode 100644 index 000000000..7dd9576b9 --- /dev/null +++ b/.changeset/pr-930.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli': patch +--- + +Prevent dev server from failing to start when the default port is already in use diff --git a/.changeset/pr-964.md b/.changeset/pr-964.md new file mode 100644 index 000000000..740372f41 --- /dev/null +++ b/.changeset/pr-964.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +propagate staging env to workbench dev server diff --git a/.changeset/pr-972.md b/.changeset/pr-972.md new file mode 100644 index 000000000..5780ee28b --- /dev/null +++ b/.changeset/pr-972.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli': patch +--- + +use the same clock for workbench locks diff --git a/.changeset/pr-988.md b/.changeset/pr-988.md new file mode 100644 index 000000000..2b3a142df --- /dev/null +++ b/.changeset/pr-988.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': minor +--- + +add promt for federation diff --git a/.changeset/pr-989.md b/.changeset/pr-989.md new file mode 100644 index 000000000..ee77cb4e7 --- /dev/null +++ b/.changeset/pr-989.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': patch +--- + +types for federation promt diff --git a/.changeset/pr-992.md b/.changeset/pr-992.md new file mode 100644 index 000000000..365f00859 --- /dev/null +++ b/.changeset/pr-992.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': minor +--- + +use `workbench` dist-tag for `sanity` package diff --git a/.changeset/pr-997.md b/.changeset/pr-997.md new file mode 100644 index 000000000..b9341929c --- /dev/null +++ b/.changeset/pr-997.md @@ -0,0 +1,6 @@ + +--- +'@sanity/cli': minor +--- + +extract manifests for local applications diff --git a/.changeset/sdk-1277-workbench-resolve-org-id.md b/.changeset/sdk-1277-workbench-resolve-org-id.md new file mode 100644 index 000000000..9ae64f63c --- /dev/null +++ b/.changeset/sdk-1277-workbench-resolve-org-id.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli': minor +--- + +Resolve the workbench organization ID from the configured project when `app.organizationId` is not set in the CLI config diff --git a/.changeset/workbench-pass-appid.md b/.changeset/workbench-pass-appid.md new file mode 100644 index 000000000..f75be2f06 --- /dev/null +++ b/.changeset/workbench-pass-appid.md @@ -0,0 +1,5 @@ +--- +'@sanity/cli': minor +--- + +Workbench now displays each local application's title and icon and can match local applications to their remote counterparts by ID. diff --git a/.github/workflows/snapshot-release.yml b/.github/workflows/snapshot-release.yml index 76ffcf5ef..4ec24a00a 100644 --- a/.github/workflows/snapshot-release.yml +++ b/.github/workflows/snapshot-release.yml @@ -1,12 +1,17 @@ name: Snapshot Release on: + push: + branches: + # Note: this needs to be removed before the feature branch gets merged to main + - feat/workbench workflow_dispatch: inputs: tag: description: 'npm dist-tag for the snapshot release' required: false - default: 'snapshot' + # TODO: revert to "snapshot" before the feature branch gets merged to main + default: 'workbench' type: string forceBump: description: 'Force a version bump for all packages' @@ -58,83 +63,17 @@ jobs: - name: Configure npm registry run: echo '//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}' > .npmrc - - - name: Generate changeset for all packages - if: ${{ inputs.forceBump != 'false' }} - run: | - PACKAGES=$(pnpm ls -r --json --depth -1 | jq -r '.[] | select(.private != true) | .name') - { - echo '---' - for pkg in $PACKAGES; do - echo "'${pkg}': ${{ inputs.forceBump }}" - done - echo '---' - echo '' - echo 'Force snapshot release' - } > .changeset/force-snapshot.md - - name: Create snapshot versions - if: ${{ inputs.forceBump == 'false' }} run: pnpm changeset version --snapshot env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - - name: Create release versions - if: ${{ inputs.forceBump != 'false' }} - run: pnpm changeset version - env: - GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - - - name: Commit and push version changes - if: ${{ inputs.forceBump != 'false' }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" - git add -A - git commit -m "chore: version packages" - git push origin HEAD:${{ github.ref_name }} - env: - GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - - name: Build packages run: pnpm run build:cli - name: Publish snapshot packages - if: ${{ inputs.forceBump == 'false' }} - run: pnpm changeset publish --tag "$DIST_TAG" 2>&1 | tee publish-output.txt + run: pnpm changeset publish --tag "workbench" 2>&1 | tee publish-output.txt env: DIST_TAG: ${{ inputs.tag }} NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} NPM_CONFIG_PROVENANCE: true - - - name: Publish release packages - if: ${{ inputs.forceBump != 'false' }} - run: pnpm changeset publish 2>&1 | tee publish-output.txt - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} - NPM_CONFIG_PROVENANCE: true - - - name: Push tags and create GitHub releases - if: ${{ inputs.forceBump != 'false' }} - run: | - git push origin --tags - for tag in $(git tag --points-at HEAD); do - gh release create "$tag" --generate-notes - done - env: - GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} - - - name: Summary - run: | - echo "## Snapshot Release" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**dist-tag:** \`${{ inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "**branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY - echo "**commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Published packages" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - grep 'New tag:' publish-output.txt >> $GITHUB_STEP_SUMMARY || echo "No packages published" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/fixtures/federated-studio/.gitignore b/fixtures/federated-studio/.gitignore new file mode 100644 index 000000000..d083f5728 --- /dev/null +++ b/fixtures/federated-studio/.gitignore @@ -0,0 +1,31 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Compiled Sanity Studio +/dist + +# Temporary Sanity runtime, generated by the CLI on every dev server start +/.sanity + +# Logs +/logs +*.log + +# Coverage directory used by testing tools +/coverage + +# Misc +.DS_Store +*.pem + +# Typescript +*.tsbuildinfo + +# Dotenv and similar local-only files +*.local + +.__mf__temp \ No newline at end of file diff --git a/fixtures/federated-studio/package.json b/fixtures/federated-studio/package.json new file mode 100644 index 000000000..d3d18cb18 --- /dev/null +++ b/fixtures/federated-studio/package.json @@ -0,0 +1,24 @@ +{ + "name": "federated-studio", + "version": "1.0.0", + "private": true, + "keywords": [ + "sanity" + ], + "license": "MIT", + "type": "module", + "scripts": { + "dev": "sanity dev", + "build:fixture": "sanity build" + }, + "dependencies": { + "react": "^19.2.5", + "react-dom": "^19.2.5", + "sanity": "catalog:", + "styled-components": "^6.4.0" + }, + "devDependencies": { + "@types/react": "^19.2.14", + "typescript": "^5.9.3" + } +} diff --git a/fixtures/federated-studio/sanity.cli.ts b/fixtures/federated-studio/sanity.cli.ts new file mode 100644 index 000000000..2ab1cd728 --- /dev/null +++ b/fixtures/federated-studio/sanity.cli.ts @@ -0,0 +1,14 @@ +import {defineCliConfig} from 'sanity/cli' + +export default defineCliConfig({ + api: { + dataset: 'test', + projectId: 'ppsg7ml5', + }, + deployment: { + autoUpdates: true, + }, + federation: { + enabled: true, + }, +}) diff --git a/fixtures/federated-studio/sanity.config.ts b/fixtures/federated-studio/sanity.config.ts new file mode 100644 index 000000000..e3717115f --- /dev/null +++ b/fixtures/federated-studio/sanity.config.ts @@ -0,0 +1,17 @@ +import {defineConfig} from 'sanity' +import {structureTool} from 'sanity/structure' + +import {schemaTypes} from './schemaTypes' + +export default defineConfig({ + title: 'Basic Studio', + + dataset: 'test', + projectId: 'ppsg7ml5', + + plugins: [structureTool()], + + schema: { + types: schemaTypes, + }, +}) diff --git a/fixtures/federated-studio/schemaTypes/index.ts b/fixtures/federated-studio/schemaTypes/index.ts new file mode 100644 index 000000000..ba4681d69 --- /dev/null +++ b/fixtures/federated-studio/schemaTypes/index.ts @@ -0,0 +1 @@ +export const schemaTypes = [] diff --git a/fixtures/federated-studio/tsconfig.json b/fixtures/federated-studio/tsconfig.json new file mode 100644 index 000000000..a8c164b14 --- /dev/null +++ b/fixtures/federated-studio/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "Preserve", + "moduleDetection": "force", + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/@sanity/cli-core/src/_exports/index.ts b/packages/@sanity/cli-core/src/_exports/index.ts index b748ae2cf..68f76e5af 100644 --- a/packages/@sanity/cli-core/src/_exports/index.ts +++ b/packages/@sanity/cli-core/src/_exports/index.ts @@ -38,6 +38,7 @@ export {type Output, type SanityOrgUser} from '../types.js' export {doImport} from '../util/doImport.js' export * from '../util/environment/mockBrowserEnvironment.js' export * from '../util/getLocalPackageVersion.js' +export * from '../util/getSanityConfigDir.js' export * from '../util/getSanityEnvVar.js' export * from '../util/getSanityUrl.js' export * from '../util/importModule.js' diff --git a/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts b/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts index 06034d561..40339da6e 100644 --- a/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts +++ b/packages/@sanity/cli-core/src/config/cli/getCliConfig.ts @@ -1,3 +1,5 @@ +import {createRequire} from 'node:module' + import {debug} from '../../debug.js' import {NotFoundError} from '../../errors/NotFoundError.js' import {importModule} from '../../util/importModule.js' @@ -18,6 +20,10 @@ const cache = new Map>() * * If loading fails the cached promise is evicted so the next call retries. * + * Long-lived processes that need to observe edits to `sanity.cli.(ts|js)` + * (e.g. a dev-server watcher) should use {@link getCliConfigUncached} + * instead — it bypasses both this in-memory cache and Node's module cache. + * * @param rootPath - Root path for the project, eg where `sanity.cli.(ts|js)` is located. * @returns The CLI config * @internal @@ -28,7 +34,7 @@ export function getCliConfig(rootPath: string): Promise { return cached } - const promise = loadCliConfig(rootPath).catch((err) => { + const promise = getCliConfigUncached(rootPath).catch((err) => { cache.delete(rootPath) throw err }) @@ -37,7 +43,23 @@ export function getCliConfig(rootPath: string): Promise { return promise } -async function loadCliConfig(rootPath: string): Promise { +/** + * Read the CLI config for a project from disk, bypassing both the + * `getCliConfig` in-memory cache and Node's module cache. Each call locates + * `sanity.cli.(ts|js)`, drops any prior jiti compilation from `require.cache`, + * re-imports, and re-validates. + * + * Use this when the config file is expected to change during the process's + * lifetime — typically a dev-server watcher that needs the new values picked + * up after each save. One-shot CLI invocations should prefer + * {@link getCliConfig} so the prerun hook, SanityCommand helpers, and action + * files share a single load. + * + * @param rootPath - Root path for the project, eg where `sanity.cli.(ts|js)` is located. + * @returns The freshly loaded CLI config + * @internal + */ +export async function getCliConfigUncached(rootPath: string): Promise { const paths = await findPathForFiles(rootPath, ['sanity.cli.ts', 'sanity.cli.js']) const configPaths = paths.filter((path) => path.exists) @@ -55,6 +77,13 @@ async function loadCliConfig(rootPath: string): Promise { debug(`Loading CLI config from: ${configPath}`) + // Drop any cached compilation of this file from Node's CJS module cache + // (jiti compiles `sanity.cli.ts` to CJS and registers it via `require.cache`). + // Without this, repeated calls would receive the previously imported module + // even though the file on disk has changed. No-op on first load. + const cjsRequire = createRequire(import.meta.url) + delete cjsRequire.cache[configPath] + let cliConfig: CliConfig | undefined try { const result = await importModule(configPath) diff --git a/packages/@sanity/cli-core/src/config/cli/schemas.ts b/packages/@sanity/cli-core/src/config/cli/schemas.ts index f9bfb031a..bf2b9d43e 100644 --- a/packages/@sanity/cli-core/src/config/cli/schemas.ts +++ b/packages/@sanity/cli-core/src/config/cli/schemas.ts @@ -34,6 +34,12 @@ export const cliConfigSchema = z.object({ }), ), + federation: z.optional( + z.object({ + enabled: z.boolean(), + }), + ), + graphql: z.optional( z.array( z.object({ diff --git a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts index b823c078e..3a76c4389 100644 --- a/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts +++ b/packages/@sanity/cli-core/src/config/cli/types/cliConfig.ts @@ -59,6 +59,14 @@ export interface CliConfig { autoUpdates?: boolean } + /** + * Enable federated builds & dev environments for your studio or application. + * @experimental + */ + federation?: { + enabled: boolean + } + /** Define the GraphQL APIs that the CLI can deploy and interact with */ graphql?: Array<{ filterSuffix?: string diff --git a/packages/@sanity/cli-core/src/services/cliUserConfig.ts b/packages/@sanity/cli-core/src/services/cliUserConfig.ts index e5d45fe8f..02d534d13 100644 --- a/packages/@sanity/cli-core/src/services/cliUserConfig.ts +++ b/packages/@sanity/cli-core/src/services/cliUserConfig.ts @@ -1,10 +1,10 @@ import {mkdirSync} from 'node:fs' -import {homedir} from 'node:os' import {dirname, join as joinPath} from 'node:path' import {z} from 'zod/mini' import {debug} from '../debug.js' +import {getSanityConfigDir} from '../util/getSanityConfigDir.js' import {readJsonFileSync} from '../util/readJsonFileSync.js' import {writeJsonFileSync} from '../util/writeJsonFileSync.js' import {clearCliTokenCache} from './cliTokenCache.js' @@ -156,10 +156,5 @@ function readConfig(): Record { * @internal */ function getCliUserConfigPath() { - const sanityEnvSuffix = process.env.SANITY_INTERNAL_ENV === 'staging' ? '-staging' : '' - const cliConfigPath = - process.env.SANITY_CLI_CONFIG_PATH || - joinPath(homedir(), '.config', `sanity${sanityEnvSuffix}`, 'config.json') - - return cliConfigPath + return process.env.SANITY_CLI_CONFIG_PATH || joinPath(getSanityConfigDir(), 'config.json') } diff --git a/packages/@sanity/cli-core/src/util/getSanityConfigDir.ts b/packages/@sanity/cli-core/src/util/getSanityConfigDir.ts new file mode 100644 index 000000000..64c5d4324 --- /dev/null +++ b/packages/@sanity/cli-core/src/util/getSanityConfigDir.ts @@ -0,0 +1,36 @@ +import {homedir} from 'node:os' +import {join as joinPath} from 'node:path' + +import {isStaging} from './isStaging.js' + +function envSuffix(): string { + return isStaging() ? '-staging' : '' +} + +/** + * Returns the base Sanity configuration directory for the current user. + * For persistent user settings (auth tokens, preferences, etc.). + * Respects `SANITY_INTERNAL_ENV=staging` to isolate staging instances. + * + * Layout: `~/.config/sanity{-staging}/` + * + * @returns Absolute path to the config directory + * @internal + */ +export function getSanityConfigDir(): string { + return joinPath(homedir(), '.config', `sanity${envSuffix()}`) +} + +/** + * Returns the base Sanity data directory for the current user. + * For ephemeral runtime state (dev-server registries, caches, etc.). + * Respects `SANITY_INTERNAL_ENV=staging` to isolate staging instances. + * + * Layout: `~/.sanity{-staging}/` + * + * @returns Absolute path to the data directory + * @internal + */ +export function getSanityDataDir(): string { + return joinPath(homedir(), `.sanity${envSuffix()}`) +} diff --git a/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts b/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts index 15c24928f..601e8de78 100644 --- a/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts +++ b/packages/@sanity/cli-e2e/__tests__/init/init.app.test.ts @@ -100,6 +100,7 @@ describe('sanity init - app', {timeout: 120_000}, () => { tmp.path, '--no-git', '--no-mcp', + '--no-federation', ], interactive: true, }) diff --git a/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts b/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts index a8425574b..c8999164a 100644 --- a/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts +++ b/packages/@sanity/cli-e2e/__tests__/init/init.studio-interactive.test.ts @@ -60,6 +60,7 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { 'pnpm', '--no-mcp', '--no-git', + '--no-federation', ], interactive: true, }) @@ -90,6 +91,7 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { tmp.path, '--no-mcp', '--no-git', + '--no-federation', ], interactive: true, }) @@ -129,6 +131,7 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { '--typescript', '--no-mcp', '--no-git', + '--no-federation', ], interactive: true, }) @@ -159,6 +162,7 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { '--package-manager', 'pnpm', '--no-git', + '--no-federation', ], interactive: true, }) @@ -191,6 +195,7 @@ describe('sanity init - studio (interactive)', {timeout: 120_000}, () => { '--package-manager', 'pnpm', '--no-git', + '--no-federation', ], interactive: true, }) diff --git a/packages/@sanity/cli-test/src/test/constants.ts b/packages/@sanity/cli-test/src/test/constants.ts index 00ef308f4..93f42de8d 100644 --- a/packages/@sanity/cli-test/src/test/constants.ts +++ b/packages/@sanity/cli-test/src/test/constants.ts @@ -15,6 +15,7 @@ export const DEFAULT_FIXTURES: Record = { 'basic-app': {}, 'basic-functions': {}, 'basic-studio': {}, + 'federated-studio': {}, 'graphql-studio': {}, 'multi-workspace-studio': {}, 'nextjs-app': {}, @@ -31,6 +32,7 @@ export type FixtureName = | 'basic-app' | 'basic-functions' | 'basic-studio' + | 'federated-studio' | 'graphql-studio' | 'multi-workspace-studio' | 'nextjs-app' diff --git a/packages/@sanity/cli/package.json b/packages/@sanity/cli/package.json index 14242818e..e3a4a027d 100644 --- a/packages/@sanity/cli/package.json +++ b/packages/@sanity/cli/package.json @@ -80,6 +80,7 @@ "@sanity/codegen": "catalog:", "@sanity/descriptors": "^1.3.0", "@sanity/export": "^6.1.0", + "@sanity/federation": "0.1.0-alpha.7", "@sanity/generate-help-url": "^4.0.0", "@sanity/id-utils": "^1.0.0", "@sanity/import": "^6.0.1", diff --git a/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts b/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts new file mode 100644 index 000000000..b1c3e1a50 --- /dev/null +++ b/packages/@sanity/cli/src/actions/build/__tests__/buildStaticFiles.test.ts @@ -0,0 +1,139 @@ +import {convertToSystemPath} from '@sanity/cli-test' +import {type InlineConfig} from 'vite' +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import {buildStaticFiles} from '../buildStaticFiles.js' + +const mockBuildApp = vi.hoisted(() => vi.fn().mockResolvedValue(undefined)) +const mockCreateBuilder = vi.hoisted(() => vi.fn().mockResolvedValue({buildApp: mockBuildApp})) +const mockBuild = vi.hoisted(() => + vi.fn().mockResolvedValue({output: [{modules: {}, name: 'test', type: 'chunk'}]}), +) +const mockGetViteConfig = vi.hoisted(() => vi.fn()) +const mockExtendViteConfigWithUserConfig = vi.hoisted(() => vi.fn()) +const mockFinalizeViteConfig = vi.hoisted(() => vi.fn()) +const mockWriteSanityRuntime = vi.hoisted(() => vi.fn()) +const mockResolveEntries = vi.hoisted(() => vi.fn()) + +vi.mock('vite', () => ({ + build: mockBuild, + createBuilder: mockCreateBuilder, +})) + +vi.mock('../getViteConfig.js', () => ({ + extendViteConfigWithUserConfig: mockExtendViteConfigWithUserConfig, + finalizeViteConfig: mockFinalizeViteConfig, + getViteConfig: mockGetViteConfig, +})) + +vi.mock('../writeSanityRuntime.js', () => ({ + resolveEntries: mockResolveEntries, + writeSanityRuntime: mockWriteSanityRuntime, +})) + +vi.mock('@sanity/cli-build/_internal', () => ({ + buildDebug: vi.fn(), + copyDir: vi.fn().mockResolvedValue(undefined), + writeFavicons: vi.fn().mockResolvedValue(undefined), +})) + +const cwd = convertToSystemPath('/test/cwd') +const outputDir = convertToSystemPath('/test/cwd/dist') + +describe('buildStaticFiles', () => { + beforeEach(() => { + const defaultViteConfig: InlineConfig = {plugins: [{name: 'sanity-default'}], root: cwd} + mockGetViteConfig.mockResolvedValue(defaultViteConfig) + mockExtendViteConfigWithUserConfig.mockImplementation(async (_env, base, user) => + typeof user === 'function' ? user(base, _env) : {...base, ...user}, + ) + mockFinalizeViteConfig.mockImplementation(async (config) => config) + mockResolveEntries.mockResolvedValue({ + relativeConfigLocation: '../../sanity.config.ts', + relativeEntry: '../../src/App.tsx', + }) + mockWriteSanityRuntime.mockResolvedValue({ + entries: {relativeConfigLocation: null, relativeEntry: '../../src/App.tsx'}, + }) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + describe('federation enabled', () => { + test('applies user vite config so custom plugins run during build', async () => { + const userPlugin = {name: 'vanilla-extract-plugin'} + const userVite = vi.fn((config: InlineConfig) => ({ + ...config, + plugins: [...(config.plugins ?? []), userPlugin], + })) + + await buildStaticFiles({ + basePath: '/', + cwd, + federation: {enabled: true}, + outputDir, + vite: userVite, + }) + + expect(mockExtendViteConfigWithUserConfig).toHaveBeenCalledWith( + {command: 'build', mode: 'production'}, + expect.objectContaining({root: cwd}), + userVite, + ) + + // Config passed to createBuilder must contain the user plugin — otherwise + // transforms like vanilla-extract never run on `.css.ts` files. + const builderConfig = mockCreateBuilder.mock.calls[0][0] + expect(builderConfig.plugins).toContainEqual(userPlugin) + + // Federation builds must not call finalizeViteConfig; it forces a + // Studio-specific entry the federation environment does not use. + expect(mockFinalizeViteConfig).not.toHaveBeenCalled() + + expect(mockBuildApp).toHaveBeenCalled() + }) + + test('skips user config merge when no user vite config is provided', async () => { + await buildStaticFiles({ + basePath: '/', + cwd, + federation: {enabled: true}, + outputDir, + }) + + expect(mockExtendViteConfigWithUserConfig).not.toHaveBeenCalled() + expect(mockBuildApp).toHaveBeenCalled() + }) + + test('does not write sanity runtime or copy static files', async () => { + await buildStaticFiles({ + basePath: '/', + cwd, + federation: {enabled: true}, + outputDir, + }) + + expect(mockWriteSanityRuntime).not.toHaveBeenCalled() + expect(mockBuild).not.toHaveBeenCalled() + }) + }) + + describe('federation disabled', () => { + test('still merges user vite config via finalizeViteConfig', async () => { + const userVite = {define: {CUSTOM: '"value"'}} + + await buildStaticFiles({ + basePath: '/', + cwd, + outputDir, + vite: userVite, + }) + + expect(mockExtendViteConfigWithUserConfig).toHaveBeenCalled() + expect(mockFinalizeViteConfig).toHaveBeenCalled() + expect(mockBuild).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts b/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts index 5cea7cd13..8a10d007c 100644 --- a/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts +++ b/packages/@sanity/cli/src/actions/build/__tests__/getViteConfig.test.ts @@ -13,6 +13,7 @@ import { } from '../getViteConfig.js' const mockExtractSchemaPlugin = vi.hoisted(() => vi.fn()) +const mockFederationPlugin = vi.hoisted(() => vi.fn()) const mockTypegenPlugin = vi.hoisted(() => vi.fn()) // Mock all external dependencies @@ -61,6 +62,12 @@ vi.mock('../../../server/vite/plugin-sanity-runtime-rewrite.js', () => ({ sanityRuntimeRewritePlugin: vi.fn(() => ({name: 'sanity-runtime-rewrite'})), })) +vi.mock('@sanity/federation/vite', () => ({ + federation: mockFederationPlugin.mockReturnValue({ + name: 'sanity/federation', + }), +})) + vi.mock('../../../server/vite/plugin-schema-extraction.js', () => ({ sanitySchemaExtractionPlugin: mockExtractSchemaPlugin.mockReturnValue({ name: 'sanity/schema-extraction', @@ -78,12 +85,17 @@ vi.mock('@sanity/cli-core', async (importOriginal) => { return { ...actual, findProjectRoot: vi.fn().mockResolvedValue({path: '/mock/config/path'}), + readPackageJson: vi.fn().mockResolvedValue({name: 'sanity'}), } }) const mockTestCwd = convertToSystemPath('/test/cwd') const mockSanityPath = convertToSystemPath('/mock/path/to/sanity') const mockCustomOutput = convertToSystemPath('/custom/output') +const mockEntries = { + relativeConfigLocation: '../../sanity.config.ts', + relativeEntry: '../../src/App', +} describe('#getViteConfig', () => { beforeEach(async () => { @@ -101,6 +113,7 @@ describe('#getViteConfig', () => { test('should create basic vite config with default options', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, } @@ -122,7 +135,7 @@ describe('#getViteConfig', () => { server: { host: undefined, port: 3333, - strictPort: true, + strictPort: false, }, }) @@ -148,6 +161,7 @@ describe('#getViteConfig', () => { test('should create vite config for app mode', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, isApp: true, mode: 'development' as const, reactCompiler: undefined, @@ -171,6 +185,7 @@ describe('#getViteConfig', () => { test('should create production config with minification', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, minify: true, mode: 'production' as const, outputDir: mockCustomOutput, @@ -201,6 +216,7 @@ describe('#getViteConfig', () => { test('should create production config without minification', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, minify: false, mode: 'production' as const, reactCompiler: undefined, @@ -217,6 +233,7 @@ describe('#getViteConfig', () => { const options = { basePath: 'custom/path', cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, } @@ -229,6 +246,7 @@ describe('#getViteConfig', () => { test('should handle custom server options', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, server: { @@ -242,7 +260,7 @@ describe('#getViteConfig', () => { expect(config.server).toMatchObject({ host: '0.0.0.0', port: 8080, - strictPort: true, + strictPort: false, }) }) @@ -256,6 +274,7 @@ describe('#getViteConfig', () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: reactCompilerConfig, } @@ -277,6 +296,7 @@ describe('#getViteConfig', () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, } @@ -296,6 +316,7 @@ describe('#getViteConfig', () => { const options = { cwd: mockTestCwd, + entries: mockEntries, importMap, mode: 'production' as const, reactCompiler: undefined, @@ -315,12 +336,31 @@ describe('#getViteConfig', () => { }) }) + test('should throw error when sanity package path cannot be resolved', async () => { + const {getDefaultFaviconsPath} = await import('@sanity/cli-build/_internal') + vi.mocked(getDefaultFaviconsPath).mockRejectedValue( + new Error('Unable to resolve `@sanity/cli-build` module root'), + ) + + const options = { + cwd: mockTestCwd, + entries: mockEntries, + mode: 'development' as const, + reactCompiler: undefined, + } + + await expect(getViteConfig(options)).rejects.toThrow( + 'Unable to resolve `@sanity/cli-build` module root', + ) + }) + test('should configure favicon plugin with correct paths', async () => { const {sanityFaviconsPlugin} = await import('../../../server/vite/plugin-sanity-favicons.js') const options = { basePath: '/studio', cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, } @@ -337,6 +377,7 @@ describe('#getViteConfig', () => { test('should include schema extraction plugin when enabled', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, schemaExtraction: { @@ -369,6 +410,7 @@ describe('#getViteConfig', () => { test('should not include schema extraction plugin when disabled', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, schemaExtraction: { @@ -390,6 +432,7 @@ describe('#getViteConfig', () => { test('should include typegen plugin when enabled', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, typegen: { @@ -420,6 +463,7 @@ describe('#getViteConfig', () => { test('should not include typegen plugin when disabled', async () => { const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'development' as const, reactCompiler: undefined, typegen: { @@ -437,6 +481,107 @@ describe('#getViteConfig', () => { expect(mockTypegenPlugin).not.toHaveBeenCalled() expect(typegenPlugin).toBeUndefined() }) + + test('should include federation plugin when enabled', async () => { + const options = { + cwd: mockTestCwd, + entries: {relativeConfigLocation: '../../sanity.config.ts', relativeEntry: '../../src/App'}, + federation: {enabled: true}, + mode: 'development' as const, + reactCompiler: undefined, + } + + const config = await getViteConfig(options) + + const federationPlugin = config.plugins?.find( + (p) => p && typeof p === 'object' && 'name' in p && p.name === 'sanity/federation', + ) + + expect(mockFederationPlugin).toHaveBeenCalledWith({ + isApp: false, + pkgJson: {name: 'sanity'}, + studioConfigPath: '../../sanity.config.ts', + workDir: mockTestCwd, + }) + expect(federationPlugin).toBeDefined() + }) + + test('should not include federation plugin when disabled', async () => { + const options = { + cwd: mockTestCwd, + entries: mockEntries, + federation: {enabled: false}, + mode: 'development' as const, + reactCompiler: undefined, + } + + const config = await getViteConfig(options) + + const federationPlugin = config.plugins?.find( + (p) => p && typeof p === 'object' && 'name' in p && p.name === 'sanity/federation', + ) + + expect(mockFederationPlugin).not.toHaveBeenCalled() + expect(federationPlugin).toBeUndefined() + }) + + test('should pass reactRefreshHost to viteReact when provided', async () => { + const viteReactMock = (await import('@vitejs/plugin-react')).default as unknown as ReturnType< + typeof vi.fn + > + + const options = { + cwd: mockTestCwd, + entries: {relativeConfigLocation: '../../sanity.config.ts', relativeEntry: '../../src/App'}, + federation: {enabled: true}, + mode: 'development' as const, + reactCompiler: undefined, + reactRefreshHost: 'http://localhost:3333', + } + + await getViteConfig(options) + + expect(viteReactMock).toHaveBeenCalledWith( + expect.objectContaining({reactRefreshHost: 'http://localhost:3333'}), + ) + }) + + test('should not pass reactRefreshHost to viteReact when not provided', async () => { + const viteReactMock = (await import('@vitejs/plugin-react')).default as unknown as ReturnType< + typeof vi.fn + > + + const options = { + cwd: mockTestCwd, + entries: mockEntries, + mode: 'development' as const, + reactCompiler: undefined, + } + + await getViteConfig(options) + + expect(viteReactMock).toHaveBeenCalledWith( + expect.not.objectContaining({reactRefreshHost: expect.anything()}), + ) + }) + + test('should not include federation plugin when federation is undefined', async () => { + const options = { + cwd: mockTestCwd, + entries: mockEntries, + mode: 'development' as const, + reactCompiler: undefined, + } + + const config = await getViteConfig(options) + + const federationPlugin = config.plugins?.find( + (p) => p && typeof p === 'object' && 'name' in p && p.name === 'sanity/federation', + ) + + expect(mockFederationPlugin).not.toHaveBeenCalled() + expect(federationPlugin).toBeUndefined() + }) }) describe('#finalizeViteConfig', () => { @@ -607,6 +752,7 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => { // which includes the onwarn callback const options = { cwd: mockTestCwd, + entries: mockEntries, mode: 'production' as const, reactCompiler: undefined, } @@ -636,6 +782,7 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => { const config = await getViteConfig({ cwd: mockTestCwd, + entries: mockEntries, mode: 'production' as const, reactCompiler: undefined, }) @@ -658,6 +805,7 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => { const config = await getViteConfig({ cwd: mockTestCwd, + entries: mockEntries, mode: 'production' as const, reactCompiler: undefined, }) @@ -679,6 +827,7 @@ describe('#onRollupWarn and #suppressUnusedImport helper functions', () => { const config = await getViteConfig({ cwd: mockTestCwd, + entries: mockEntries, mode: 'production' as const, reactCompiler: undefined, }) diff --git a/packages/@sanity/cli/src/actions/build/buildApp.ts b/packages/@sanity/cli/src/actions/build/buildApp.ts index 8bdce91bb..09ef92495 100644 --- a/packages/@sanity/cli/src/actions/build/buildApp.ts +++ b/packages/@sanity/cli/src/actions/build/buildApp.ts @@ -35,6 +35,7 @@ interface InternalBuildOptions { calledFromDeploy: boolean | undefined determineBasePath: () => string entry: string | undefined + federation: CliConfig['federation'] minify: boolean outDir: string | undefined output: Output @@ -62,6 +63,7 @@ export async function buildApp(options: BuildOptions): Promise { calledFromDeploy: options.calledFromDeploy, determineBasePath: () => determineBasePath(cliConfig, 'app', output), entry: cliConfig && 'app' in cliConfig ? cliConfig.app?.entry : undefined, + federation: cliConfig.federation, minify: flags.minify, outDir, output, @@ -204,7 +206,7 @@ async function internalBuildApp(options: InternalBuildOptions): Promise { let importMap: {imports?: Record} | undefined - if (autoUpdatesEnabled) { + if (autoUpdatesEnabled && !options.federation?.enabled) { importMap = { imports: { ...(await buildVendorDependencies({basePath, cwd: workDir, isApp: true, outputDir})), @@ -222,6 +224,7 @@ async function internalBuildApp(options: InternalBuildOptions): Promise { basePath, cwd: workDir, entry: options.entry, + federation: options.federation, importMap, isApp: true, minify: options.minify, diff --git a/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts b/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts index bedea1686..e6ac88c8d 100644 --- a/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts +++ b/packages/@sanity/cli/src/actions/build/buildStaticFiles.ts @@ -3,10 +3,10 @@ import path from 'node:path' import {buildDebug, copyDir, writeFavicons} from '@sanity/cli-build/_internal' import {type CliConfig, type UserViteConfig} from '@sanity/cli-core' import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler' -import {build} from 'vite' +import {build, createBuilder} from 'vite' import {extendViteConfigWithUserConfig, finalizeViteConfig, getViteConfig} from './getViteConfig.js' -import {writeSanityRuntime} from './writeSanityRuntime.js' +import {resolveEntries, writeSanityRuntime} from './writeSanityRuntime.js' export interface ChunkModule { name: string @@ -19,7 +19,7 @@ export interface ChunkStats { name: string } -interface StaticBuildOptions { +interface StaticBuildOptions extends Pick { basePath: string cwd: string outputDir: string @@ -51,6 +51,7 @@ export async function buildStaticFiles( basePath, cwd, entry, + federation, importMap, isApp, minify = true, @@ -61,8 +62,52 @@ export async function buildStaticFiles( vite: extendViteConfig, } = options + const mode = 'production' + + /* Federation builds only produce the federation environment + * (remote-entry, mf-manifest) — skip client-specific steps like + * runtime generation, static file copies, and favicons. + */ + if (federation?.enabled) { + buildDebug('Resolving entries for federation build') + const entries = await resolveEntries({cwd, entry, isApp}) + + buildDebug('Resolving vite config (federation)') + let viteConfig = await getViteConfig({ + basePath, + cwd, + entries, + federation, + isApp, + minify, + mode, + outputDir, + reactCompiler, + sourceMap, + }) + + // Apply the user's Vite config so plugins like `@vanilla-extract/vite-plugin` + // transform source files before the federation environment is bundled. + // `finalizeViteConfig` is intentionally skipped: the federation environment + // has its own entry and does not use `.sanity/runtime/app.js`. + if (extendViteConfig) { + viteConfig = await extendViteConfigWithUserConfig( + {command: 'build', mode}, + viteConfig, + extendViteConfig, + ) + } + + buildDebug('Bundling federation environment') + const builder = await createBuilder(viteConfig) + await builder.buildApp() + buildDebug('Bundling complete') + // TODO: add stats here + return {chunks: []} + } + buildDebug('Writing Sanity runtime files') - await writeSanityRuntime({ + const {entries} = await writeSanityRuntime({ appTitle, basePath, cwd, @@ -73,11 +118,12 @@ export async function buildStaticFiles( }) buildDebug('Resolving vite config') - const mode = 'production' let viteConfig = await getViteConfig({ autoUpdatesCssUrls, basePath, cwd, + entries, + federation, importMap, isApp, minify, diff --git a/packages/@sanity/cli/src/actions/build/buildStudio.ts b/packages/@sanity/cli/src/actions/build/buildStudio.ts index f0b5ca413..66a665dfe 100644 --- a/packages/@sanity/cli/src/actions/build/buildStudio.ts +++ b/packages/@sanity/cli/src/actions/build/buildStudio.ts @@ -37,6 +37,7 @@ interface InternalBuildOptions { autoUpdatesEnabled: boolean calledFromDeploy: boolean | undefined determineBasePath: () => string + federation: CliConfig['federation'] isApp: boolean minify: boolean outDir: string | undefined @@ -77,6 +78,7 @@ export async function buildStudio(options: BuildOptions): Promise { autoUpdatesEnabled: options.autoUpdatesEnabled, calledFromDeploy, determineBasePath: () => determineBasePath(cliConfig, 'studio', output), + federation: cliConfig.federation, isApp: determineIsApp(cliConfig), minify: Boolean(flags.minify), outDir, @@ -274,7 +276,7 @@ async function internalBuildStudio(options: InternalBuildOptions): Promise let importMap - if (autoUpdatesEnabled) { + if (autoUpdatesEnabled && !options.federation?.enabled) { importMap = { imports: { ...(await buildVendorDependencies({basePath, cwd: workDir, isApp: false, outputDir})), @@ -290,6 +292,7 @@ async function internalBuildStudio(options: InternalBuildOptions): Promise autoUpdatesCssUrls: autoUpdatesCssUrls.length > 0 ? autoUpdatesCssUrls : undefined, basePath, cwd: workDir, + federation: options.federation, importMap, minify, outputDir, diff --git a/packages/@sanity/cli/src/actions/build/getViteConfig.ts b/packages/@sanity/cli/src/actions/build/getViteConfig.ts index 0ee5ba52c..bd264d9f5 100644 --- a/packages/@sanity/cli/src/actions/build/getViteConfig.ts +++ b/packages/@sanity/cli/src/actions/build/getViteConfig.ts @@ -5,12 +5,14 @@ import { type CliConfig, findProjectRoot, getCliTelemetry, + readPackageJson, type UserViteConfig, } from '@sanity/cli-core' +import {federation as viteFederation} from '@sanity/federation/vite' import viteReact from '@vitejs/plugin-react' import {type PluginOptions as ReactCompilerConfig} from 'babel-plugin-react-compiler' import debug from 'debug' -import {type ConfigEnv, type InlineConfig, mergeConfig, type Rollup} from 'vite' +import {type ConfigEnv, type InlineConfig, mergeConfig, type PluginOption, type Rollup} from 'vite' import {SANITY_CACHE_DIR} from '../../constants.js' import {sanityBuildEntries} from '../../server/vite/plugin-sanity-build-entries.js' @@ -25,12 +27,17 @@ import { } from './getEnvironmentVariables.js' import {normalizeBasePath} from './normalizeBasePath.js' -interface ViteOptions { +interface ViteOptions extends Pick { /** * Root path of the studio/sanity app */ cwd: string + entries: { + relativeConfigLocation: string | null + relativeEntry: string + } + /** * Mode to run vite in - eg development or production */ @@ -63,9 +70,11 @@ interface ViteOptions { */ outputDir?: string /** - * Schema extraction configuration + * URL of the workbench dev server for react-refresh in federated builds. + * Passed as `reactRefreshHost` to `@vitejs/plugin-react` so the refresh + * preamble connects to the host application's HMR server. */ - schemaExtraction?: CliConfig['schemaExtraction'] + reactRefreshHost?: string /** * HTTP development server configuration */ @@ -74,10 +83,6 @@ interface ViteOptions { * Whether or not to enable source maps */ sourceMap?: boolean - /** - * Typegen configuration - */ - typegen?: CliConfig['typegen'] } /** @@ -90,12 +95,15 @@ export async function getViteConfig(options: ViteOptions): Promise autoUpdatesCssUrls, basePath: rawBasePath = '/', cwd, + entries, + federation, importMap, isApp, minify, mode, outputDir, reactCompiler, + reactRefreshHost, schemaExtraction, server, // default to `true` when `mode=development` @@ -115,6 +123,42 @@ export async function getViteConfig(options: ViteOptions): Promise ? getAppEnvironmentVariables({jsonEncode: true, prefix: 'process.env.'}) : getStudioEnvironmentVariables({jsonEncode: true, prefix: 'process.env.'}) + const sharedPlugins: PluginOption = [ + viteReact({ + ...(reactCompiler + ? { + babel: { + generatorOpts: {compact: true}, + plugins: [['babel-plugin-react-compiler', reactCompiler]], + }, + } + : {}), + ...(reactRefreshHost ? {reactRefreshHost} : {}), + }), + ...(schemaExtraction?.enabled + ? [ + sanitySchemaExtractionPlugin({ + additionalPatterns: schemaExtraction.watchPatterns, + configPath, + enforceRequiredFields: schemaExtraction.enforceRequiredFields, + outputPath: schemaExtraction.path, + telemetryLogger: getCliTelemetry(), + workDir: cwd, + workspaceName: schemaExtraction.workspace, + }), + ] + : []), + ...(typegen?.enabled + ? [ + sanityTypegenPlugin({ + config: typegen, + telemetryLogger: getCliTelemetry(), + workDir: cwd, + }), + ] + : []), + ] + const viteConfig: InlineConfig = { base: basePath, build: { @@ -146,43 +190,36 @@ export async function getViteConfig(options: ViteOptions): Promise logLevel: mode === 'production' ? 'silent' : 'info', mode, plugins: [ - viteReact( - reactCompiler - ? { - babel: { - generatorOpts: {compact: true}, - plugins: [['babel-plugin-react-compiler', reactCompiler]], - }, - } - : {}, - ), - sanityFaviconsPlugin({customFaviconsPath, defaultFaviconsPath, staticUrlPath: staticPath}), - sanityRuntimeRewritePlugin(), - sanityBuildEntries({autoUpdatesCssUrls, basePath, cwd, importMap, isApp}), - // Add schema extraction when enabled - ...(schemaExtraction?.enabled + // Federation builds only need the federation plugin — skip client-specific + // plugins (react, favicons, runtime rewrite, build entries, schema, typegen) + ...(federation?.enabled ? [ - sanitySchemaExtractionPlugin({ - additionalPatterns: schemaExtraction.watchPatterns, - configPath, - enforceRequiredFields: schemaExtraction.enforceRequiredFields, - outputPath: schemaExtraction.path, - telemetryLogger: getCliTelemetry(), + ...sharedPlugins, + viteFederation({ + ...(isApp + ? { + appEntry: entries.relativeEntry, + isApp: true as const, + } + : { + isApp: false as const, + // TODO: fix this non-null assertion + studioConfigPath: entries.relativeConfigLocation!, + }), + pkgJson: await readPackageJson(path.join(cwd, 'package.json')), workDir: cwd, - workspaceName: schemaExtraction.workspace, }), ] - : []), - // Add typegen when enabled - ...(typegen?.enabled - ? [ - sanityTypegenPlugin({ - config: typegen, - telemetryLogger: getCliTelemetry(), - workDir: cwd, + : [ + ...sharedPlugins, + sanityFaviconsPlugin({ + customFaviconsPath, + defaultFaviconsPath, + staticUrlPath: staticPath, }), - ] - : []), + sanityRuntimeRewritePlugin(), + sanityBuildEntries({autoUpdatesCssUrls, basePath, cwd, importMap, isApp}), + ]), ], resolve: { dedupe: ['react', 'react-dom', 'sanity', 'styled-components'], @@ -191,9 +228,7 @@ export async function getViteConfig(options: ViteOptions): Promise server: { host: server?.host, port: server?.port || 3333, - // Only enable strict port for studio, - // since apps can run on any port - strictPort: isApp ? false : true, + strictPort: false, /** * Significantly speed up startup time, @@ -208,14 +243,14 @@ export async function getViteConfig(options: ViteOptions): Promise }, } - if (mode === 'production') { + // Federation builds don't produce a client bundle — the federation + // plugin configures its own environment and build entry point. + if (mode === 'production' && !federation?.enabled) { viteConfig.build = { ...viteConfig.build, - assetsDir: 'static', emptyOutDir: false, // Rely on CLI to do this minify: minify ? 'esbuild' : false, - rollupOptions: { external: createExternalFromImportMap(importMap), input: { diff --git a/packages/@sanity/cli/src/actions/build/writeSanityRuntime.ts b/packages/@sanity/cli/src/actions/build/writeSanityRuntime.ts index 19f4d7796..bb082af60 100644 --- a/packages/@sanity/cli/src/actions/build/writeSanityRuntime.ts +++ b/packages/@sanity/cli/src/actions/build/writeSanityRuntime.ts @@ -31,7 +31,10 @@ interface RuntimeOptions { * @returns A watcher instance if watch is enabled, undefined otherwise * @internal */ -export async function writeSanityRuntime(options: RuntimeOptions): Promise { +export async function writeSanityRuntime(options: RuntimeOptions): Promise<{ + entries: {relativeConfigLocation: string | null; relativeEntry: string} + watcher: FSWatcher | undefined +}> { const {appTitle, basePath, cwd, entry, isApp, reactStrictMode, watch} = options const runtimeDir = path.join(cwd, '.sanity', 'runtime') @@ -71,6 +74,44 @@ export async function writeSanityRuntime(options: RuntimeOptions): Promise { + const {cwd, entry, isApp} = options + const runtimeDir = options.runtimeDir ?? path.join(cwd, '.sanity', 'runtime') + let relativeConfigLocation: string | null = null if (!isApp) { const studioConfigPath = await tryFindStudioConfigPath(cwd) @@ -82,16 +123,8 @@ export async function writeSanityRuntime(options: RuntimeOptions): Promise vi.fn()) +const mockStartAppDevServer = vi.hoisted(() => vi.fn()) +const mockStartStudioDevServer = vi.hoisted(() => vi.fn()) +const mockStartFederationRegistration = vi.hoisted(() => vi.fn()) +const mockGetSharedServerConfig = vi.hoisted(() => vi.fn()) + +vi.mock('../startWorkbenchDevServer.js', () => ({ + startWorkbenchDevServer: mockStartWorkbenchDevServer, +})) +vi.mock('../startAppDevServer.js', () => ({ + startAppDevServer: mockStartAppDevServer, +})) +vi.mock('../startStudioDevServer.js', () => ({ + startStudioDevServer: mockStartStudioDevServer, +})) +vi.mock('../startFederationRegistration.js', () => ({ + startFederationRegistration: mockStartFederationRegistration, +})) +vi.mock('../../../util/getSharedServerConfig.js', () => ({ + getSharedServerConfig: mockGetSharedServerConfig, +})) + +/** Create a mock Vite dev server config shape — `server.config.server.host` + * reflects the resolved host after user-provided Vite config has been merged in. */ +function mockServer({host, port = 3334}: {host?: boolean | string; port?: number} = {}) { + return { + close: vi.fn().mockResolvedValue(undefined), + server: {config: {server: {host, port}}}, + } +} + +describe('devAction', () => { + beforeEach(() => { + mockGetSharedServerConfig.mockReturnValue({httpHost: 'localhost', httpPort: 3333}) + // Default: no workbench (federation disabled) + mockStartWorkbenchDevServer.mockResolvedValue({ + close: vi.fn().mockResolvedValue(undefined), + httpHost: 'localhost', + workbenchAvailable: false, + workbenchPort: 3333, + }) + mockStartFederationRegistration.mockResolvedValue({ + close: vi.fn().mockResolvedValue(undefined), + }) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + test('studio mode without workbench uses original port', async () => { + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3333})) + + await devAction(createBaseDevOptions()) + + expect(mockStartStudioDevServer).toHaveBeenCalledWith( + expect.objectContaining({flags: expect.objectContaining({port: '3333'})}), + ) + }) + + test('studio mode with workbench bumps port and logs workbench URL', async () => { + mockStartWorkbenchDevServer.mockResolvedValue({ + close: vi.fn().mockResolvedValue(undefined), + httpHost: 'localhost', + workbenchAvailable: true, + workbenchPort: 3333, + }) + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) + const output = createMockOutput() + + await devAction(createBaseDevOptions({output})) + + expect(mockStartStudioDevServer).toHaveBeenCalledWith( + expect.objectContaining({ + flags: expect.objectContaining({port: '3334'}), + workbenchAvailable: true, + }), + ) + expect(output.log).toHaveBeenCalledWith(expect.stringContaining('3333')) + expect(output.log).toHaveBeenCalledWith(expect.stringContaining('3334')) + }) + + test('app mode routes to startAppDevServer', async () => { + mockStartAppDevServer.mockResolvedValue(mockServer({port: 3333})) + + await devAction(createBaseDevOptions({isApp: true})) + + expect(mockStartAppDevServer).toHaveBeenCalled() + expect(mockStartStudioDevServer).not.toHaveBeenCalled() + }) + + test('passes reactRefreshHost pointing to workbench when workbench is running', async () => { + mockStartWorkbenchDevServer.mockResolvedValue({ + close: vi.fn().mockResolvedValue(undefined), + httpHost: 'localhost', + workbenchAvailable: true, + workbenchPort: 3333, + }) + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) + + await devAction(createBaseDevOptions()) + + expect(mockStartStudioDevServer).toHaveBeenCalledWith( + expect.objectContaining({reactRefreshHost: 'http://localhost:3333'}), + ) + }) + + test('does not pass reactRefreshHost when workbench is not running', async () => { + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3333})) + + await devAction(createBaseDevOptions()) + + expect(mockStartStudioDevServer).toHaveBeenCalledWith( + expect.objectContaining({reactRefreshHost: undefined}), + ) + }) + + test('cleans up workbench and re-throws when app/studio startup fails', async () => { + const mockWorkbenchClose = vi.fn().mockResolvedValue(undefined) + mockStartWorkbenchDevServer.mockResolvedValue({ + close: mockWorkbenchClose, + httpHost: 'localhost', + workbenchAvailable: true, + workbenchPort: 3333, + }) + const startupError = new Error('Port already in use') + mockStartStudioDevServer.mockRejectedValue(startupError) + + const thrown = await devAction(createBaseDevOptions()).catch((err) => err) + + expect(thrown).toBe(startupError) + expect(mockWorkbenchClose).toHaveBeenCalled() + }) + + test('close handler is resilient to one close rejecting', async () => { + const mockWorkbenchClose = vi.fn().mockRejectedValue(new Error('workbench close failed')) + const mockAppClose = vi.fn().mockResolvedValue(undefined) + mockStartWorkbenchDevServer.mockResolvedValue({ + close: mockWorkbenchClose, + httpHost: 'localhost', + workbenchAvailable: true, + workbenchPort: 3333, + }) + mockStartStudioDevServer.mockResolvedValue({ + ...mockServer({port: 3334}), + close: mockAppClose, + }) + + const result = await devAction(createBaseDevOptions()) + + await expect(result.close()).resolves.toBeUndefined() + expect(mockWorkbenchClose).toHaveBeenCalled() + expect(mockAppClose).toHaveBeenCalled() + }) + + describe('federation registration', () => { + test('starts federation registration when federation is enabled', async () => { + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) + + await devAction(createBaseDevOptions({cliConfig: {federation: {enabled: true}}})) + + expect(mockStartFederationRegistration).toHaveBeenCalledWith( + expect.objectContaining({ + isApp: false, + workDir: '/tmp/sanity-project', + }), + ) + }) + + test('does not start federation registration when federation is disabled', async () => { + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3333})) + + await devAction(createBaseDevOptions()) + + expect(mockStartFederationRegistration).not.toHaveBeenCalled() + }) + + test('passes isApp: true for app mode', async () => { + mockStartAppDevServer.mockResolvedValue(mockServer({port: 3334})) + + await devAction(createBaseDevOptions({cliConfig: {federation: {enabled: true}}, isApp: true})) + + expect(mockStartFederationRegistration).toHaveBeenCalledWith( + expect.objectContaining({isApp: true}), + ) + }) + + test('passes the vite dev server to federation registration', async () => { + const server = mockServer({port: 3334}) + mockStartStudioDevServer.mockResolvedValue(server) + + await devAction(createBaseDevOptions({cliConfig: {federation: {enabled: true}}})) + + expect(mockStartFederationRegistration).toHaveBeenCalledWith( + expect.objectContaining({server: server.server}), + ) + }) + + test('calls federation close on close', async () => { + const mockFederationClose = vi.fn().mockResolvedValue(undefined) + mockStartFederationRegistration.mockResolvedValue({close: mockFederationClose}) + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) + + const result = await devAction( + createBaseDevOptions({cliConfig: {federation: {enabled: true}}}), + ) + + await result.close() + expect(mockFederationClose).toHaveBeenCalled() + }) + }) + + test('registers signal handlers that trigger close on SIGINT', async () => { + const mockWorkbenchClose = vi.fn().mockResolvedValue(undefined) + const mockAppClose = vi.fn().mockResolvedValue(undefined) + mockStartWorkbenchDevServer.mockResolvedValue({ + close: mockWorkbenchClose, + httpHost: 'localhost', + workbenchAvailable: false, + workbenchPort: 3333, + }) + mockStartStudioDevServer.mockResolvedValue({ + ...mockServer({port: 3333}), + close: mockAppClose, + }) + + await devAction(createBaseDevOptions()) + + expect(process.listenerCount('SIGINT')).toBeGreaterThanOrEqual(1) + expect(process.listenerCount('SIGTERM')).toBeGreaterThanOrEqual(1) + }) + + test('close removes signal handlers', async () => { + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3333})) + + const sigintBefore = process.listenerCount('SIGINT') + const sigtermBefore = process.listenerCount('SIGTERM') + + const result = await devAction(createBaseDevOptions()) + + expect(process.listenerCount('SIGINT')).toBe(sigintBefore + 1) + expect(process.listenerCount('SIGTERM')).toBe(sigtermBefore + 1) + + await result.close() + + expect(process.listenerCount('SIGINT')).toBe(sigintBefore) + expect(process.listenerCount('SIGTERM')).toBe(sigtermBefore) + }) + + test('uses local httpHost for workbench URL even when existing lock reports a different host', async () => { + // Scenario: process A started workbench on mydev.local:3333, process B starts + // with --host localhost. startWorkbenchDevServer returns the existing lock's host, + // but devAction ignores it and uses its own httpHost from getSharedServerConfig. + mockGetSharedServerConfig.mockReturnValue({httpHost: 'localhost', httpPort: 3333}) + mockStartWorkbenchDevServer.mockResolvedValue({ + close: vi.fn().mockResolvedValue(undefined), + httpHost: 'mydev.local', + workbenchAvailable: true, + workbenchPort: 3333, + }) + mockStartStudioDevServer.mockResolvedValue(mockServer({port: 3334})) + const output = createMockOutput() + + await devAction(createBaseDevOptions({output})) + + // The workbench is running on mydev.local — the URL and reactRefreshHost + // should reflect the existing workbench's host, not the caller's. + expect(output.log).toHaveBeenCalledWith(expect.stringContaining('mydev.local:3333')) + expect(mockStartStudioDevServer).toHaveBeenCalledWith( + expect.objectContaining({reactRefreshHost: 'http://mydev.local:3333'}), + ) + }) + + test('returns early with workbench-only close when app server exits without a server', async () => { + // startAppDevServer resolves with {} when orgId is missing — no `server`. + const mockWorkbenchClose = vi.fn().mockResolvedValue(undefined) + mockStartWorkbenchDevServer.mockResolvedValue({ + close: mockWorkbenchClose, + httpHost: 'localhost', + workbenchAvailable: false, + workbenchPort: 3333, + }) + mockStartAppDevServer.mockResolvedValue({}) + + const result = await devAction(createBaseDevOptions({isApp: true})) + + expect(result.close).toBeDefined() + // The close must still tear down the workbench server + await result.close() + expect(mockWorkbenchClose).toHaveBeenCalled() + // No registration should have happened because federation wasn't evaluated + expect(mockStartFederationRegistration).not.toHaveBeenCalled() + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/devServerRegistry.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/devServerRegistry.test.ts new file mode 100644 index 000000000..06bb54841 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/devServerRegistry.test.ts @@ -0,0 +1,705 @@ +import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import { + __resetStartTimeCacheForTesting, + acquireWorkbenchLock, + type DevServerManifest, + getRegisteredServers, + readWorkbenchLock, + registerDevServer, + watchRegistry, +} from '../devServerRegistry.js' + +const mockExecSync = vi.hoisted(() => vi.fn()) + +vi.mock('node:child_process', () => ({ + execSync: mockExecSync, +})) + +// Mock getSanityConfigDir to use a temp directory per test +let testDataDir: string + +vi.mock('@sanity/cli-core', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getSanityDataDir: () => testDataDir, + } +}) + +/** Derives the registry path the same way the module does internally. */ +function registryDir() { + return join(testDataDir, 'dev-servers') +} + +beforeEach(() => { + testDataDir = join(tmpdir(), `sanity-registry-test-${process.pid}-${Date.now()}`) + mkdirSync(testDataDir, {recursive: true}) + // By default, return a start time matching "now" so our own PID passes isOurProcess + mockExecSync.mockReturnValue(new Date().toString()) + __resetStartTimeCacheForTesting() +}) + +afterEach(() => { + vi.clearAllMocks() + vi.unstubAllEnvs() +}) + +describe('registerDevServer', () => { + test('writes a manifest file and returns a cleanup function', () => { + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + + const filePath = join(registryDir(), `${process.pid}.json`) + expect(existsSync(filePath)).toBe(true) + + const manifest = JSON.parse(readFileSync(filePath, 'utf8')) + expect(manifest.pid).toBe(process.pid) + expect(manifest.type).toBe('studio') + expect(manifest.port).toBe(3334) + expect(manifest.host).toBe('localhost') + expect(manifest.workDir).toBe('/tmp/project') + expect(manifest.startedAt).toBeDefined() + expect(manifest.version).toBe(1) + + cleanup() + expect(existsSync(filePath)).toBe(false) + }) + + test('persists id in the manifest when provided', () => { + const {release: cleanup} = registerDevServer({ + host: 'localhost', + id: 'app-abc', + port: 3334, + type: 'coreApp', + workDir: '/tmp/project', + }) + + const manifest = JSON.parse(readFileSync(join(registryDir(), `${process.pid}.json`), 'utf8')) + expect(manifest.id).toBe('app-abc') + + cleanup() + }) + + test('persists projectId in the manifest when provided', () => { + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + projectId: 'x1g7jygt', + type: 'studio', + workDir: '/tmp/project', + }) + + const manifest = JSON.parse(readFileSync(join(registryDir(), `${process.pid}.json`), 'utf8')) + expect(manifest.projectId).toBe('x1g7jygt') + + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + expect(servers[0].projectId).toBe('x1g7jygt') + + cleanup() + }) + + test('omits optional metadata when not provided and retains manifest through getRegisteredServers', () => { + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + + const manifest = JSON.parse(readFileSync(join(registryDir(), `${process.pid}.json`), 'utf8')) + expect(manifest.id).toBeUndefined() + expect(manifest.manifest).toBeUndefined() + + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + expect(servers[0].id).toBeUndefined() + expect(servers[0].manifest).toBeUndefined() + + cleanup() + }) + + test('update inlines the extracted manifest into the registry entry', () => { + const {release: cleanup, update} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + + const inlined = {createdAt: '2026-01-01T00:00:00.000Z', version: 3, workspaces: []} + update({manifest: inlined, manifestUpdatedAt: '2026-01-01T00:00:00.000Z'}) + + const servers = getRegisteredServers() + expect(servers[0].manifest).toEqual(inlined) + expect(servers[0].manifestUpdatedAt).toBe('2026-01-01T00:00:00.000Z') + + cleanup() + }) + + test('cleanup does not throw if file already removed', () => { + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3333, + type: 'studio', + workDir: '/tmp/project', + }) + + cleanup() + expect(() => cleanup()).not.toThrow() + }) + + test('update after release is a no-op — late background extractions do not re-create the file', () => { + const {release: cleanup, update} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + + const filePath = join(registryDir(), `${process.pid}.json`) + cleanup() + expect(existsSync(filePath)).toBe(false) + + // Simulate a background extraction completing after release. + update({ + manifest: {createdAt: '2026-01-01T00:00:00.000Z', version: 3, workspaces: []}, + manifestUpdatedAt: '2026-01-01T00:00:00.000Z', + }) + + expect(existsSync(filePath)).toBe(false) + }) +}) + +describe('acquireWorkbenchLock', () => { + test('returns lock object when lock is available', () => { + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(lock).toBeDefined() + expect(lock!.release).toBeTypeOf('function') + expect(lock!.updatePort).toBeTypeOf('function') + lock!.release() + }) + + test('returns undefined when lock is already held by a live process', () => { + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(lock).toBeDefined() + + const second = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(second).toBeUndefined() + + lock!.release() + }) + + test('release removes the lock file', () => { + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + lock!.release() + + const second = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(second).toBeDefined() + second!.release() + }) + + test('stores host, port, startedAt and version in the lock file', () => { + const lock = acquireWorkbenchLock({host: '0.0.0.0', port: 4000}) + + const lockPath = join(registryDir(), 'workbench.lock') + const data = JSON.parse(readFileSync(lockPath, 'utf8')) + expect(data.host).toBe('0.0.0.0') + expect(data.port).toBe(4000) + expect(data.pid).toBe(process.pid) + expect(data.startedAt).toBeDefined() + expect(data.version).toBe(1) + + lock!.release() + }) + + test('updatePort writes the new port to the lock file', () => { + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + lock!.updatePort(3334) + + const lockPath = join(registryDir(), 'workbench.lock') + const data = JSON.parse(readFileSync(lockPath, 'utf8')) + expect(data.port).toBe(3334) + + lock!.release() + }) + + test('reclaims stale lock from a dead process', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + writeFileSync( + join(dir, 'workbench.lock'), + JSON.stringify({ + host: 'localhost', + pid: 99_999_999, + port: 3333, + startedAt: new Date().toISOString(), + version: 1, + }), + {flag: 'wx'}, + ) + + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(lock).toBeDefined() + lock!.release() + }) + + test('returns undefined after exhausting retries', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + // Write a lock that will fail schema validation (missing required fields), + // causing readWorkbenchLock to return undefined without pruning the file. + // With retries=0, the function should not recurse. + writeFileSync(join(dir, 'workbench.lock'), JSON.stringify({host: 'localhost', pid: 1, port: 1})) + + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}, 0) + expect(lock).toBeUndefined() + }) +}) + +describe('readWorkbenchLock', () => { + test('returns undefined when no lock file exists', () => { + expect(readWorkbenchLock()).toBeUndefined() + }) + + test('returns lock data when lock is held by a live process', () => { + const lock = acquireWorkbenchLock({host: '0.0.0.0', port: 4000}) + + const data = readWorkbenchLock() + expect(data).toBeDefined() + expect(data!.host).toBe('0.0.0.0') + expect(data!.port).toBe(4000) + expect(data!.pid).toBe(process.pid) + + lock!.release() + }) + + test('prunes stale lock and returns undefined', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const lockPath = join(dir, 'workbench.lock') + writeFileSync( + lockPath, + JSON.stringify({ + host: 'localhost', + pid: 99_999_999, + port: 3333, + startedAt: new Date().toISOString(), + version: 1, + }), + ) + + expect(readWorkbenchLock()).toBeUndefined() + expect(existsSync(lockPath)).toBe(false) + }) + + // Regression: a zero-byte lock (e.g. left behind by a crashed writer or a + // killed `sanity dev` mid-write) used to early-return undefined without + // pruning, so the next acquire attempt hit EEXIST forever — `sanity dev` + // logged "Workbench dev server started at …" while no Vite was actually + // listening. + test('prunes zero-byte lock and returns undefined', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const lockPath = join(dir, 'workbench.lock') + writeFileSync(lockPath, '') + + expect(readWorkbenchLock()).toBeUndefined() + expect(existsSync(lockPath)).toBe(false) + }) + + test('prunes unparsable-JSON lock and returns undefined', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const lockPath = join(dir, 'workbench.lock') + writeFileSync(lockPath, 'not json {{{') + + expect(readWorkbenchLock()).toBeUndefined() + expect(existsSync(lockPath)).toBe(false) + }) +}) + +describe('PID-reuse detection', () => { + test('prunes manifest when PID is alive but start time does not match', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + // Write a manifest for the current PID but with a startedAt far in the past + const staleManifest: DevServerManifest = { + host: 'localhost', + pid: process.pid, + port: 9999, + startedAt: new Date('2020-01-01T00:00:00Z').toISOString(), + type: 'studio', + version: 1, + workDir: '/tmp/stale-project', + } + writeFileSync(join(dir, `${process.pid}.json`), JSON.stringify(staleManifest)) + + // Mock ps to return a different (current) start time + mockExecSync.mockReturnValue(new Date().toString()) + + const servers = getRegisteredServers() + expect(servers).toHaveLength(0) + expect(existsSync(join(dir, `${process.pid}.json`))).toBe(false) + }) + + test('keeps manifest when PID is alive and start time matches', () => { + const now = new Date() + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const manifest: DevServerManifest = { + host: 'localhost', + pid: process.pid, + port: 9999, + startedAt: now.toISOString(), + type: 'studio', + version: 1, + workDir: '/tmp/valid-project', + } + writeFileSync(join(dir, `${process.pid}.json`), JSON.stringify(manifest)) + + // Mock ps to return matching start time + mockExecSync.mockReturnValue(now.toString()) + + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + expect(servers[0].port).toBe(9999) + }) + + test('falls back to alive-check when start time cannot be retrieved', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const manifest: DevServerManifest = { + host: 'localhost', + pid: process.pid, + port: 9999, + startedAt: new Date().toISOString(), + type: 'studio', + version: 1, + workDir: '/tmp/fallback-project', + } + writeFileSync(join(dir, `${process.pid}.json`), JSON.stringify(manifest)) + + // ps fails — should fall back to isProcessAlive (which will be true for our PID) + mockExecSync.mockImplementation(() => { + throw new Error('ps not available') + }) + + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + }) + + test('prunes workbench lock when PID is reused', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const lockPath = join(dir, 'workbench.lock') + writeFileSync( + lockPath, + JSON.stringify({ + host: 'localhost', + pid: process.pid, + port: 4000, + startedAt: new Date('2020-01-01T00:00:00Z').toISOString(), + version: 1, + }), + ) + + mockExecSync.mockReturnValue(new Date().toString()) + + expect(readWorkbenchLock()).toBeUndefined() + expect(existsSync(lockPath)).toBe(false) + }) +}) + +describe('Windows / win32', () => { + let originalPlatform: NodeJS.Platform + + beforeEach(() => { + originalPlatform = process.platform + Object.defineProperty(process, 'platform', {configurable: true, value: 'win32'}) + __resetStartTimeCacheForTesting() + }) + + afterEach(() => { + Object.defineProperty(process, 'platform', {configurable: true, value: originalPlatform}) + }) + + test('shells out to PowerShell with -NoProfile -NonInteractive', () => { + mockExecSync.mockReturnValue('2026-04-17T11:38:10.0000000+00:00') + + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: 'C:\\projects\\win', + }) + + expect(mockExecSync).toHaveBeenCalled() + const cmd = mockExecSync.mock.calls[0][0] as string + expect(cmd).toMatch(/^powershell\.exe /) + expect(cmd).toContain('-NoProfile') + expect(cmd).toContain('-NonInteractive') + expect(cmd).toContain('Get-CimInstance Win32_Process') + expect(cmd).toContain(`ProcessId=${process.pid}`) + expect(cmd).toContain("CreationDate.ToString('o')") + + cleanup() + }) + + test('parses PowerShell ISO 8601 round-trip output', () => { + const osStart = new Date('2026-04-17T11:38:10.000Z') + mockExecSync.mockReturnValue('2026-04-17T11:38:10.0000000+00:00') + + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: 'C:\\projects\\win', + }) + + const manifest = JSON.parse(readFileSync(join(registryDir(), `${process.pid}.json`), 'utf8')) + expect(new Date(manifest.startedAt).getTime()).toBe(osStart.getTime()) + + cleanup() + }) + + test('detects PID reuse on Windows (PowerShell start time mismatch)', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const staleManifest: DevServerManifest = { + host: 'localhost', + pid: process.pid, + port: 9999, + startedAt: '2020-01-01T00:00:00.000Z', + type: 'studio', + version: 1, + workDir: 'C:\\projects\\stale', + } + writeFileSync(join(dir, `${process.pid}.json`), JSON.stringify(staleManifest)) + + mockExecSync.mockReturnValue('2026-04-17T11:38:10.0000000+00:00') + + const servers = getRegisteredServers() + expect(servers).toHaveLength(0) + expect(existsSync(join(dir, `${process.pid}.json`))).toBe(false) + }) + + test('falls back to alive-check when PowerShell fails (e.g. no PowerShell)', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + const manifest: DevServerManifest = { + host: 'localhost', + pid: process.pid, + port: 9999, + startedAt: new Date().toISOString(), + type: 'studio', + version: 1, + workDir: 'C:\\projects\\fallback', + } + writeFileSync(join(dir, `${process.pid}.json`), JSON.stringify(manifest)) + + mockExecSync.mockImplementation(() => { + throw new Error("'powershell.exe' is not recognized") + }) + + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + }) + + test("memoises own PID's start time across calls (avoids repeated PowerShell spawns)", () => { + mockExecSync.mockReturnValue('2026-04-17T11:38:10.0000000+00:00') + + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: 'C:\\projects\\cache', + }) + + const callsAfterRegistration = mockExecSync.mock.calls.length + + // Each of these recomputes isOurProcess(process.pid, ...) → would + // shell out again without the cache. + getRegisteredServers() + getRegisteredServers() + getRegisteredServers() + + expect(mockExecSync.mock.calls.length).toBe(callsAfterRegistration) + + cleanup() + }) +}) + +describe('startedAt uses OS-reported process start time', () => { + // Regression: previously `registerDevServer` / `acquireWorkbenchLock` stored + // `new Date().toISOString()` at call time, but `isOurProcess` compares that + // value against `ps -o lstart=` with a 2s tolerance. In real `sanity dev` + // runs, registration happens several seconds after process start (after the + // workbench + app Vite servers boot), so the drift exceeded the tolerance + // and manifests were pruned as "stale" the moment the watcher re-read them. + + test('registerDevServer stores the OS-reported start time, not the call time', () => { + const osStart = new Date('2026-04-17T11:38:10.000Z') + mockExecSync.mockReturnValue(osStart.toString()) + + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + + const manifest = JSON.parse(readFileSync(join(registryDir(), `${process.pid}.json`), 'utf8')) + // `new Date(osStart.toString())` loses sub-second precision the same way + // `getProcessStartTime` does, so this is an exact match. + expect(manifest.startedAt).toBe(new Date(osStart.toString()).toISOString()) + + cleanup() + }) + + test('manifest written several seconds after OS process start survives pruning', () => { + // Simulate reality: the process started 5s ago, registerDevServer is only + // called now (after the CLI booted its Vite servers). Before the fix, the + // manifest's startedAt would be "now" and mismatch the ps-reported time + // by 5s — well past the 2s tolerance — so getRegisteredServers would + // immediately prune it. + const osStart = new Date(Date.now() - 5000) + mockExecSync.mockReturnValue(osStart.toString()) + + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + expect(servers[0].port).toBe(3334) + expect(existsSync(join(registryDir(), `${process.pid}.json`))).toBe(true) + + cleanup() + }) + + test('acquireWorkbenchLock stores the OS-reported start time, not the call time', () => { + const osStart = new Date('2026-04-17T11:38:10.000Z') + mockExecSync.mockReturnValue(osStart.toString()) + + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(lock).toBeDefined() + + const data = JSON.parse(readFileSync(join(registryDir(), 'workbench.lock'), 'utf8')) + expect(data.startedAt).toBe(new Date(osStart.toString()).toISOString()) + + lock!.release() + }) + + test('lock written several seconds after OS process start survives readWorkbenchLock', () => { + const osStart = new Date(Date.now() - 5000) + mockExecSync.mockReturnValue(osStart.toString()) + + const lock = acquireWorkbenchLock({host: 'localhost', port: 3333}) + expect(lock).toBeDefined() + + // readWorkbenchLock re-runs isOurProcess — with the fix, the stored + // startedAt matches the ps-reported time, so the lock is considered live. + const read = readWorkbenchLock() + expect(read).toBeDefined() + expect(read!.port).toBe(3333) + + lock!.release() + }) + + test('falls back to new Date() when ps is unavailable', () => { + mockExecSync.mockImplementation(() => { + throw new Error('ps not available') + }) + + const before = Date.now() + const {release: cleanup} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: '/tmp/project', + }) + const after = Date.now() + + const manifest = JSON.parse(readFileSync(join(registryDir(), `${process.pid}.json`), 'utf8')) + const storedMs = new Date(manifest.startedAt).getTime() + expect(storedMs).toBeGreaterThanOrEqual(before) + expect(storedMs).toBeLessThanOrEqual(after) + + cleanup() + }) +}) + +describe('watchRegistry', () => { + test('invokes callback when a manifest file is added', async () => { + const callback = vi.fn() + const watcher = watchRegistry(callback) + + const dir = registryDir() + const manifest: DevServerManifest = { + host: 'localhost', + pid: process.pid, + port: 5555, + startedAt: new Date().toISOString(), + type: 'studio', + version: 1, + workDir: '/tmp/watch-test', + } + writeFileSync(join(dir, `${process.pid}.json`), JSON.stringify(manifest)) + + await new Promise((resolve) => setTimeout(resolve, 500)) + + expect(callback).toHaveBeenCalled() + const servers = callback.mock.calls.at(-1)![0] + expect(servers.some((s: DevServerManifest) => s.port === 5555)).toBe(true) + + watcher.close() + }) + + test('close stops notifications', async () => { + const callback = vi.fn() + const watcher = watchRegistry(callback) + watcher.close() + + const dir = registryDir() + writeFileSync( + join(dir, 'after-close.json'), + JSON.stringify({ + host: 'localhost', + pid: process.pid, + port: 6666, + startedAt: new Date().toISOString(), + type: 'studio', + version: 1, + workDir: '/tmp/closed', + }), + ) + + await new Promise((resolve) => setTimeout(resolve, 500)) + + expect(callback).not.toHaveBeenCalled() + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/devServerRegistry.windows.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/devServerRegistry.windows.test.ts new file mode 100644 index 000000000..dc5351e80 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/devServerRegistry.windows.test.ts @@ -0,0 +1,128 @@ +/** + * Windows-only integration tests for {@link getProcessStartTime} and the + * registry/lock plumbing that depends on it. + * + * Unlike `devServerRegistry.test.ts`, this file does **not** mock + * `node:child_process` — it shells out to a real PowerShell so we catch + * regressions where the command doesn't run, the output format drifts, or + * `Get-Process` behaves differently on a real Windows host. + * + * Skipped on macOS / Linux. The matching `windows-8core` shards in CI run + * this file end-to-end. + */ +import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'node:fs' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import { + __resetStartTimeCacheForTesting, + acquireWorkbenchLock, + getRegisteredServers, + readWorkbenchLock, + registerDevServer, +} from '../devServerRegistry.js' + +let testDataDir: string + +vi.mock('@sanity/cli-core', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getSanityDataDir: () => testDataDir, + } +}) + +beforeEach(() => { + testDataDir = join(tmpdir(), `sanity-registry-win-${process.pid}-${Date.now()}`) + mkdirSync(testDataDir, {recursive: true}) + __resetStartTimeCacheForTesting() +}) + +afterEach(() => { + __resetStartTimeCacheForTesting() +}) + +function registryDir() { + return join(testDataDir, 'dev-servers') +} + +describe.skipIf(process.platform !== 'win32')('Windows integration', () => { + test('getProcessStartTime returns a Date close to now for our PID via real PowerShell', () => { + const {release} = registerDevServer({ + host: 'localhost', + port: 3334, + type: 'studio', + workDir: testDataDir, + }) + + const manifestPath = join(registryDir(), `${process.pid}.json`) + expect(existsSync(manifestPath)).toBe(true) + + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) + const startedAt = new Date(manifest.startedAt) + expect(Number.isNaN(startedAt.getTime())).toBe(false) + + // The start time must predate "now" but not be older than this test's + // process — generous bound covers slow-runner cold-starts. + const ageMs = Date.now() - startedAt.getTime() + expect(ageMs).toBeGreaterThanOrEqual(0) + expect(ageMs).toBeLessThan(15 * 60_000) + + release() + }) + + test('round-trip: register → re-read keeps our manifest live (PowerShell-verified)', () => { + const {release} = registerDevServer({ + host: 'localhost', + port: 3335, + type: 'studio', + workDir: testDataDir, + }) + + // Re-reading invokes isOurProcess(process.pid, ...) which goes through + // the real PowerShell branch. If the command/output drifts, the + // manifest gets pruned as "stale" and this returns 0. + const servers = getRegisteredServers() + expect(servers).toHaveLength(1) + expect(servers[0].port).toBe(3335) + + release() + }) + + test('acquireWorkbenchLock + readWorkbenchLock round-trip on Windows', () => { + const lock = acquireWorkbenchLock({host: 'localhost', port: 3336}) + expect(lock).toBeDefined() + + const read = readWorkbenchLock() + expect(read).toBeDefined() + expect(read!.pid).toBe(process.pid) + expect(read!.port).toBe(3336) + + lock!.release() + }) + + test('detects PID reuse on Windows (manually-written stale lock is pruned)', () => { + const dir = registryDir() + mkdirSync(dir, {recursive: true}) + + // Write a workbench lock claiming our PID started in the distant past. + // PowerShell will report the real (recent) start time of this test + // process, so isOurProcess sees the mismatch and prunes. + const lockPath = join(dir, 'workbench.lock') + writeFileSync( + lockPath, + JSON.stringify({ + host: 'localhost', + pid: process.pid, + port: 4000, + startedAt: '2020-01-01T00:00:00.000Z', + version: 1, + }), + ) + + expect(readWorkbenchLock()).toBeUndefined() + expect(existsSync(lockPath)).toBe(false) + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/getDashboardAppUrl.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/getDashboardAppUrl.test.ts deleted file mode 100644 index 005da8cdf..000000000 --- a/packages/@sanity/cli/src/actions/dev/__tests__/getDashboardAppUrl.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' - -import {getDashboardAppURL} from '../getDashboardAppUrl.js' - -const mockFetch = vi.fn() - -describe('#getDashboardAppUrl', () => { - beforeEach(() => { - vi.stubGlobal('fetch', mockFetch) - vi.useFakeTimers() - }) - - afterEach(() => { - vi.clearAllMocks() - vi.unstubAllGlobals() - vi.useRealTimers() - }) - - test('should send default dashboard app url if fetch timesout', async () => { - mockFetch.mockImplementation( - (_url, {signal}) => - new Promise((resolve, reject) => { - const timeout = setTimeout( - () => resolve({json: () => ({url: 'https://custom.url'}), ok: true}), - 6000, - ) - signal.addEventListener('abort', () => { - clearTimeout(timeout) - reject(new DOMException('Aborted', 'AbortError')) - }) - }), - ) - - const promise = getDashboardAppURL({ - httpHost: 'localhost', - httpPort: 3333, - organizationId: 'org-123', - }) - - await vi.advanceTimersByTimeAsync(5000) - - const result = await promise - - expect(result).toBe('https://www.sanity.io/@org-123?dev=http%3A%2F%2Flocalhost%3A3333') - }) - - test('should send default dashboard app url if fetch fails', async () => { - mockFetch.mockResolvedValue({ - ok: false, - statusText: 'Internal Server Error', - }) - - const result = await getDashboardAppURL({ - httpHost: 'localhost', - httpPort: 3333, - organizationId: 'org-456', - }) - - expect(result).toBe('https://www.sanity.io/@org-456?dev=http%3A%2F%2Flocalhost%3A3333') - }) - - test('should send default url if body does not return url', async () => { - mockFetch.mockResolvedValue({ - json: () => Promise.resolve({}), - ok: true, - }) - - const result = await getDashboardAppURL({ - httpHost: 'localhost', - httpPort: 3333, - organizationId: 'org-789', - }) - - expect(result).toBe('https://www.sanity.io/@org-789?dev=http%3A%2F%2Flocalhost%3A3333') - }) - - test('sends back dashboard app url when successful', async () => { - mockFetch.mockResolvedValue({ - json: () => Promise.resolve({url: 'https://custom-dashboard.sanity.io/@org-789?dev=test'}), - ok: true, - }) - - const result = await getDashboardAppURL({ - httpHost: 'localhost', - httpPort: 3333, - organizationId: 'org-789', - }) - - expect(result).toBe('https://custom-dashboard.sanity.io/@org-789?dev=test') - }) -}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/getDevServerConfig.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/getDevServerConfig.test.ts index b935af63f..30f0dfe75 100644 --- a/packages/@sanity/cli/src/actions/dev/__tests__/getDevServerConfig.test.ts +++ b/packages/@sanity/cli/src/actions/dev/__tests__/getDevServerConfig.test.ts @@ -23,7 +23,6 @@ const FLAGS = { 'auto-updates': false, host: 'localhost', json: false, - 'load-in-dashboard': false, port: '3333', } as const diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startAppDevServer.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startAppDevServer.test.ts new file mode 100644 index 000000000..bfa730459 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startAppDevServer.test.ts @@ -0,0 +1,146 @@ +import {type CliConfig} from '@sanity/cli-core' +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import {startAppDevServer} from '../startAppDevServer.js' +import {type DevActionOptions} from '../types.js' +import {createDevOptions, createMockOutput} from './testHelpers.js' + +const mockStartDevServer = vi.hoisted(() => vi.fn()) +const mockGracefulServerDeath = vi.hoisted(() => vi.fn()) +const mockGetDevServerConfig = vi.hoisted(() => vi.fn()) + +vi.mock('../../../server/devServer.js', () => ({ + startDevServer: mockStartDevServer, +})) +vi.mock('../../../server/gracefulServerDeath.js', () => ({ + gracefulServerDeath: mockGracefulServerDeath, +})) +vi.mock('../getDevServerConfig.js', () => ({ + getDevServerConfig: mockGetDevServerConfig, +})) + +function mockServer({port = 3333}: {port?: number} = {}) { + return { + close: vi.fn().mockResolvedValue(undefined), + server: {config: {server: {port}}}, + } +} + +function createOptions(overrides: Partial = {}): DevActionOptions { + return createDevOptions({ + cliConfig: {app: {organizationId: 'org-1'}} as unknown as CliConfig, + isApp: true, + ...overrides, + }) +} + +describe('startAppDevServer', () => { + beforeEach(() => { + mockGetDevServerConfig.mockReturnValue({ + basePath: '/', + cwd: '/tmp/sanity-project', + httpHost: 'localhost', + httpPort: 3333, + reactStrictMode: false, + staticPath: '/tmp/sanity-project/static', + }) + mockStartDevServer.mockResolvedValue(mockServer()) + mockGracefulServerDeath.mockImplementation((_cmd, _host, _port, err) => err) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + test('exits with error when organizationId is missing', async () => { + const output = createMockOutput() + const result = await startAppDevServer( + createOptions({cliConfig: {app: {}} as unknown as CliConfig, output}), + ) + + expect(output.error).toHaveBeenCalledWith( + expect.stringContaining('organization ID'), + expect.objectContaining({exit: 1}), + ) + expect(result).toEqual({}) + expect(mockStartDevServer).not.toHaveBeenCalled() + }) + + test('exits with error when cliConfig has no app property', async () => { + const output = createMockOutput() + const result = await startAppDevServer(createOptions({cliConfig: {} as CliConfig, output})) + + expect(output.error).toHaveBeenCalledWith( + expect.stringContaining('organization ID'), + expect.objectContaining({exit: 1}), + ) + expect(result).toEqual({}) + }) + + test('starts dev server with isApp and appTitle from cliConfig', async () => { + mockStartDevServer.mockResolvedValue(mockServer({port: 3334})) + + const result = await startAppDevServer( + createOptions({ + cliConfig: { + app: {organizationId: 'org-1', title: 'My App'}, + } as unknown as CliConfig, + }), + ) + + expect(mockStartDevServer).toHaveBeenCalledWith( + expect.objectContaining({ + appTitle: 'My App', + isApp: true, + }), + ) + expect(result.server).toBeDefined() + expect(result.close).toBeDefined() + }) + + test('passes reactRefreshHost through to startDevServer', async () => { + await startAppDevServer(createOptions({reactRefreshHost: 'http://localhost:3333'})) + + expect(mockStartDevServer).toHaveBeenCalledWith( + expect.objectContaining({reactRefreshHost: 'http://localhost:3333'}), + ) + }) + + test('logs "App dev server started" when workbench is not available', async () => { + mockStartDevServer.mockResolvedValue(mockServer({port: 3334})) + const output = createMockOutput() + + await startAppDevServer(createOptions({output, workbenchAvailable: false})) + + expect(output.log).toHaveBeenCalledWith(expect.stringContaining('3334')) + }) + + test('skips the port log line when workbench is available', async () => { + mockStartDevServer.mockResolvedValue(mockServer({port: 3334})) + const output = createMockOutput() + + await startAppDevServer(createOptions({output, workbenchAvailable: true})) + + // 'Starting dev server' is still logged, but the port announcement is not + const logCalls = (output.log as ReturnType).mock.calls.flat() + expect(logCalls.some((c) => String(c).includes('App dev server started'))).toBe(false) + }) + + test('wraps startup failures via gracefulServerDeath', async () => { + const originalErr = Object.assign(new Error('boom'), {code: 'EADDRINUSE'}) + const wrappedErr = new Error('friendly message') + mockStartDevServer.mockRejectedValueOnce(originalErr) + mockGracefulServerDeath.mockReturnValueOnce(wrappedErr) + + let error: unknown + try { + await startAppDevServer(createOptions()) + } catch (err) { + error = err + } + + expect(error).toBeInstanceOf(Error) + expect(error).toBe(wrappedErr) + expect(mockGracefulServerDeath).toHaveBeenCalledWith('dev', 'localhost', 3333, originalErr) + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startDevManifestWatcher.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startDevManifestWatcher.test.ts new file mode 100644 index 000000000..3418f0a17 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startDevManifestWatcher.test.ts @@ -0,0 +1,273 @@ +import {EventEmitter} from 'node:events' + +import {afterEach, beforeEach, describe, expect, type Mock, test, vi} from 'vitest' + +import {startDevManifestWatcher} from '../startDevManifestWatcher.js' +import {createMockOutput} from './testHelpers.js' + +const mockFindProjectRoot = vi.hoisted(() => vi.fn()) +const mockFsWatch = vi.hoisted(() => vi.fn()) + +vi.mock('@sanity/cli-core', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + findProjectRoot: mockFindProjectRoot, + } +}) + +vi.mock('node:fs', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + watch: mockFsWatch, + } +}) + +/** Fake FSWatcher that exposes helpers for simulating events in tests. */ +// eslint-disable-next-line unicorn/prefer-event-target -- mirrors node's FSWatcher which extends EventEmitter +class FakeFsWatcher extends EventEmitter { + public closed = false + public handler: ((event: string, filename: string | null) => void) | undefined + + close() { + this.closed = true + } + + emitChange(filename: string | null) { + this.handler?.('change', filename) + } +} + +const WORK_DIR = '/tmp/studio' +const STUDIO_CONFIG_PATH = '/tmp/studio/sanity.config.ts' + +describe('startDevManifestWatcher', () => { + let fakeWatcher: FakeFsWatcher + let mockExtract: Mock<(params: {configPath: string; workDir: string}) => Promise> + const studioManifest = {createdAt: '2026-01-01', version: 3, workspaces: []} + + beforeEach(() => { + fakeWatcher = new FakeFsWatcher() + mockExtract = vi.fn(async () => studioManifest) + mockFindProjectRoot.mockResolvedValue({ + directory: WORK_DIR, + path: STUDIO_CONFIG_PATH, + type: 'studio', + }) + mockFsWatch.mockImplementation((_dir: string, listener: FakeFsWatcher['handler']) => { + fakeWatcher.handler = listener + return fakeWatcher + }) + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + vi.clearAllMocks() + }) + + test('runs an initial extraction on startup and inlines it into update', async () => { + const update = vi.fn() + + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output: createMockOutput(), + update, + workDir: WORK_DIR, + }) + + // Flush the fire-and-forget microtask chain so the initial extraction + // has time to resolve and patch the registry. + await vi.advanceTimersByTimeAsync(0) + + expect(mockExtract).toHaveBeenCalledTimes(1) + expect(mockExtract).toHaveBeenCalledWith({ + configPath: STUDIO_CONFIG_PATH, + workDir: WORK_DIR, + }) + expect(update).toHaveBeenCalledWith({ + manifest: studioManifest, + manifestUpdatedAt: expect.any(String), + }) + + await watcher.close() + }) + + test('coalesces a config-file change that fires during the initial extraction', async () => { + // Block the first extraction until we say otherwise. This simulates the + // user editing sanity.config.ts while the worker is still producing the + // initial manifest — the watcher must not run a parallel extraction. + let resolveFirst: ((value: typeof studioManifest) => void) | undefined + mockExtract + .mockReturnValueOnce( + new Promise((resolve) => { + resolveFirst = resolve + }), + ) + .mockResolvedValueOnce(studioManifest) + + const update = vi.fn() + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output: createMockOutput(), + update, + workDir: WORK_DIR, + }) + + // Fire a change event while the initial extraction is still in-flight. + fakeWatcher.emitChange('sanity.config.ts') + await vi.advanceTimersByTimeAsync(300) + + // Only the initial extraction is running — the config change is pending. + expect(mockExtract).toHaveBeenCalledTimes(1) + + // Release the initial extraction; the pending change-triggered run now + // starts, serialized behind it. + resolveFirst!(studioManifest) + await vi.advanceTimersByTimeAsync(0) + + expect(mockExtract).toHaveBeenCalledTimes(2) + expect(update).toHaveBeenCalledTimes(2) + + await watcher.close() + }) + + test('re-extracts and inlines the new manifest after a debounced config file change', async () => { + const update = vi.fn() + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output: createMockOutput(), + update, + workDir: WORK_DIR, + }) + + // Wait for the initial extraction to complete before exercising the + // file-change path. + await vi.advanceTimersByTimeAsync(0) + expect(mockExtract).toHaveBeenCalledTimes(1) + + // Fire multiple rapid "change" events — should coalesce into a single + // regeneration after the debounce window. + fakeWatcher.emitChange('sanity.config.ts') + fakeWatcher.emitChange('sanity.config.ts') + fakeWatcher.emitChange('sanity.config.ts') + + await vi.advanceTimersByTimeAsync(300) + + expect(mockExtract).toHaveBeenCalledTimes(2) + expect(update).toHaveBeenCalledTimes(2) + + await watcher.close() + }) + + test('ignores changes to other files in the config directory', async () => { + const update = vi.fn() + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output: createMockOutput(), + update, + workDir: WORK_DIR, + }) + + await vi.advanceTimersByTimeAsync(0) + expect(mockExtract).toHaveBeenCalledTimes(1) + + fakeWatcher.emitChange('unrelated.ts') + fakeWatcher.emitChange('package.json') + + await vi.advanceTimersByTimeAsync(300) + + expect(mockExtract).toHaveBeenCalledTimes(1) + + await watcher.close() + }) + + test('logs a warning and keeps running when extraction fails', async () => { + const output = createMockOutput() + const update = vi.fn() + mockExtract.mockRejectedValueOnce(new Error('bad schema')).mockResolvedValueOnce(studioManifest) + + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output, + update, + workDir: WORK_DIR, + }) + + // The initial extraction fails. + await vi.advanceTimersByTimeAsync(0) + expect(output.warn).toHaveBeenCalledWith(expect.stringContaining('bad schema')) + expect(update).not.toHaveBeenCalled() + + // A subsequent change recovers and updates as normal. + fakeWatcher.emitChange('sanity.config.ts') + await vi.advanceTimersByTimeAsync(300) + expect(update).toHaveBeenCalledTimes(1) + + await watcher.close() + }) + + test('stops regenerating after close', async () => { + const update = vi.fn() + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output: createMockOutput(), + update, + workDir: WORK_DIR, + }) + + await vi.advanceTimersByTimeAsync(0) + expect(mockExtract).toHaveBeenCalledTimes(1) + + await watcher.close() + expect(fakeWatcher.closed).toBe(true) + + fakeWatcher.emitChange('sanity.config.ts') + await vi.advanceTimersByTimeAsync(300) + + expect(mockExtract).toHaveBeenCalledTimes(1) + }) + + test('watches sanity.cli.ts for app projects', async () => { + const APP_WORK_DIR = '/tmp/sdk-app' + const APP_CONFIG_PATH = '/tmp/sdk-app/sanity.cli.ts' + const appManifest = {icon: '', title: 'My App', version: '1'} + mockFindProjectRoot.mockResolvedValue({ + directory: APP_WORK_DIR, + path: APP_CONFIG_PATH, + type: 'app', + }) + mockExtract.mockResolvedValue(appManifest) + const update = vi.fn() + + const watcher = await startDevManifestWatcher({ + extract: mockExtract, + output: createMockOutput(), + update, + workDir: APP_WORK_DIR, + }) + + await vi.advanceTimersByTimeAsync(0) + expect(mockExtract).toHaveBeenCalledWith({ + configPath: APP_CONFIG_PATH, + workDir: APP_WORK_DIR, + }) + expect(update).toHaveBeenCalledWith({ + manifest: appManifest, + manifestUpdatedAt: expect.any(String), + }) + + // sanity.config.ts is not the app's config — should be ignored. + fakeWatcher.emitChange('sanity.config.ts') + await vi.advanceTimersByTimeAsync(300) + expect(mockExtract).toHaveBeenCalledTimes(1) + + // sanity.cli.ts saves trigger regeneration. + fakeWatcher.emitChange('sanity.cli.ts') + await vi.advanceTimersByTimeAsync(300) + expect(mockExtract).toHaveBeenCalledTimes(2) + + await watcher.close() + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts new file mode 100644 index 000000000..b174b3fc0 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startFederationRegistration.test.ts @@ -0,0 +1,306 @@ +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import {startFederationRegistration} from '../startFederationRegistration.js' +import {createMockOutput} from './testHelpers.js' + +const mockRegisterDevServer = vi.hoisted(() => vi.fn()) +const mockStartDevManifestWatcher = vi.hoisted(() => vi.fn()) +const mockExtractCoreAppManifest = vi.hoisted(() => vi.fn()) +const mockExtractStudioManifest = vi.hoisted(() => vi.fn()) +const mockCheckForDeprecatedAppId = vi.hoisted(() => vi.fn()) +const mockGetAppId = vi.hoisted(() => vi.fn()) + +vi.mock('../devServerRegistry.js', () => ({ + registerDevServer: mockRegisterDevServer, +})) +vi.mock('../startDevManifestWatcher.js', () => ({ + startDevManifestWatcher: mockStartDevManifestWatcher, +})) +vi.mock('../../manifest/extractCoreAppManifest.js', () => ({ + extractCoreAppManifest: mockExtractCoreAppManifest, +})) +vi.mock('../extractDevServerManifest.js', () => ({ + extractStudioManifest: mockExtractStudioManifest, +})) +vi.mock('../../../util/appId.js', () => ({ + checkForDeprecatedAppId: mockCheckForDeprecatedAppId, + getAppId: mockGetAppId, +})) + +function mockServer({host, port = 3334}: {host?: boolean | string; port?: number} = {}) { + return { + config: {server: {host, port}}, + httpServer: {address: () => ({address: '127.0.0.1', family: 'IPv4', port})}, + } +} + +describe('startFederationRegistration', () => { + beforeEach(() => { + mockRegisterDevServer.mockReturnValue({release: vi.fn(), update: vi.fn()}) + mockStartDevManifestWatcher.mockResolvedValue({close: vi.fn().mockResolvedValue(undefined)}) + mockExtractCoreAppManifest.mockResolvedValue(undefined) + mockGetAppId.mockReturnValue(undefined) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + test('registers studio in registry', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockRegisterDevServer).toHaveBeenCalledWith( + expect.objectContaining({ + host: 'localhost', + port: 3334, + type: 'studio', + }), + ) + }) + + test('passes deployment.appId to registerDevServer', async () => { + mockGetAppId.mockReturnValue('app-abc') + + await startFederationRegistration({ + cliConfig: {deployment: {appId: 'app-abc'}, federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockRegisterDevServer).toHaveBeenCalledWith(expect.objectContaining({id: 'app-abc'})) + }) + + test('forwards api.projectId to registerDevServer', async () => { + await startFederationRegistration({ + cliConfig: {api: {projectId: 'x1g7jygt'}, federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockRegisterDevServer).toHaveBeenCalledWith( + expect.objectContaining({projectId: 'x1g7jygt'}), + ) + }) + + test('omits projectId when api.projectId is not configured', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + const [registerArg] = mockRegisterDevServer.mock.calls[0] + expect(registerArg.projectId).toBeUndefined() + }) + + test('checks for deprecated app.id', async () => { + const output = createMockOutput() + + await startFederationRegistration({ + cliConfig: {app: {id: 'legacy-app'}, federation: {enabled: true}}, + isApp: false, + output, + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockCheckForDeprecatedAppId).toHaveBeenCalledWith(expect.objectContaining({output})) + }) + + test('registers without icon/title — they are derived from the inlined manifest', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + const [registerArg] = mockRegisterDevServer.mock.calls[0] + expect(registerArg).not.toHaveProperty('icon') + expect(registerArg).not.toHaveProperty('title') + }) + + test('registers app under the host applied by the vite dev server', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({host: 'mydev.local', port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockRegisterDevServer).toHaveBeenCalledWith( + expect.objectContaining({host: 'mydev.local'}), + ) + }) + + test('falls back to localhost when the vite server host is not a string', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({host: true, port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockRegisterDevServer).toHaveBeenCalledWith(expect.objectContaining({host: 'localhost'})) + }) + + test('registers app type when isApp is true', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: true, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockRegisterDevServer).toHaveBeenCalledWith(expect.objectContaining({type: 'coreApp'})) + }) + + test('starts the manifest watcher for studios', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockStartDevManifestWatcher).toHaveBeenCalledWith( + expect.objectContaining({workDir: '/tmp/sanity-project'}), + ) + }) + + test('starts the manifest watcher for core apps', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: true, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(mockStartDevManifestWatcher).toHaveBeenCalledWith( + expect.objectContaining({extract: expect.any(Function), workDir: '/tmp/sanity-project'}), + ) + }) + + test('wires extractCoreAppManifest into the core-app watcher', async () => { + const appManifest = {icon: '', title: 'My App', version: '1'} + mockExtractCoreAppManifest.mockResolvedValue(appManifest) + + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: true, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + const {extract} = mockStartDevManifestWatcher.mock.calls[0][0] + await expect( + extract({configPath: '/tmp/sanity-project/sanity.cli.ts', workDir: '/tmp/sanity-project'}), + ).resolves.toEqual(appManifest) + expect(mockExtractCoreAppManifest).toHaveBeenCalledWith({workDir: '/tmp/sanity-project'}) + }) + + test('wires extractStudioManifest into the studio watcher', async () => { + await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + const {extract} = mockStartDevManifestWatcher.mock.calls[0][0] + expect(extract).toBe(mockExtractStudioManifest) + }) + + test('calls manifest cleanup on close', async () => { + const mockCleanup = vi.fn() + mockRegisterDevServer.mockReturnValue({release: mockCleanup, update: vi.fn()}) + + const result = await startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + await result.close() + expect(mockCleanup).toHaveBeenCalled() + }) + + test('propagates error when registerDevServer throws', async () => { + const error = new Error('Registry write failed') + mockRegisterDevServer.mockImplementation(() => { + throw error + }) + + await expect( + startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }), + ).rejects.toThrow(error) + }) + + test('propagates error when startDevManifestWatcher rejects', async () => { + const error = new Error('Watcher setup failed') + mockStartDevManifestWatcher.mockRejectedValue(error) + + await expect( + startFederationRegistration({ + cliConfig: {federation: {enabled: true}}, + isApp: false, + output: createMockOutput(), + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }), + ).rejects.toThrow(error) + }) + + test('calls output.error when both app.id and deployment.appId are set', async () => { + mockCheckForDeprecatedAppId.mockImplementation(({output}: {output: any}) => { + output.error('Found both app.id (deprecated) and deployment.appId', {exit: 1}) + }) + + const output = createMockOutput() + + await startFederationRegistration({ + cliConfig: { + app: {id: 'legacy-app'}, + deployment: {appId: 'new-app'}, + federation: {enabled: true}, + }, + isApp: false, + output, + server: mockServer({port: 3334}) as any, + workDir: '/tmp/sanity-project', + }) + + expect(output.error).toHaveBeenCalledWith( + expect.stringContaining('Found both app.id (deprecated) and deployment.appId'), + expect.objectContaining({exit: 1}), + ) + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startStudioDevServer.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startStudioDevServer.test.ts new file mode 100644 index 000000000..8c4b61c90 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startStudioDevServer.test.ts @@ -0,0 +1,267 @@ +import {type CliConfig} from '@sanity/cli-core' +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import {startStudioDevServer} from '../startStudioDevServer.js' +import {createDevOptions, createMockOutput} from './testHelpers.js' + +const mockStartDevServer = vi.hoisted(() => vi.fn()) +const mockGracefulServerDeath = vi.hoisted(() => vi.fn()) +const mockGetDevServerConfig = vi.hoisted(() => vi.fn()) +const mockCheckStudioDependencyVersions = vi.hoisted(() => vi.fn()) +const mockCheckRequiredDependencies = vi.hoisted(() => vi.fn()) +const mockShouldAutoUpdate = vi.hoisted(() => vi.fn()) +const mockCompareDependencyVersions = vi.hoisted(() => vi.fn()) +const mockGetLocalPackageVersion = vi.hoisted(() => vi.fn()) +const mockGetAppId = vi.hoisted(() => vi.fn()) +const mockGetPackageManagerChoice = vi.hoisted(() => vi.fn()) +const mockUpgradePackages = vi.hoisted(() => vi.fn()) +const mockIsInteractive = vi.hoisted(() => vi.fn()) +const mockConfirm = vi.hoisted(() => vi.fn()) + +vi.mock('../../../server/devServer.js', () => ({ + startDevServer: mockStartDevServer, +})) +vi.mock('../../../server/gracefulServerDeath.js', () => ({ + gracefulServerDeath: mockGracefulServerDeath, +})) +vi.mock('../getDevServerConfig.js', () => ({ + getDevServerConfig: mockGetDevServerConfig, +})) +vi.mock('@sanity/cli-build/_internal', () => ({ + checkStudioDependencyVersions: mockCheckStudioDependencyVersions, +})) +vi.mock('../../build/checkRequiredDependencies.js', () => ({ + checkRequiredDependencies: mockCheckRequiredDependencies, +})) +vi.mock('../../build/shouldAutoUpdate.js', () => ({ + shouldAutoUpdate: mockShouldAutoUpdate, +})) +vi.mock('../../../util/compareDependencyVersions.js', () => ({ + compareDependencyVersions: mockCompareDependencyVersions, +})) +vi.mock('../../../util/appId.js', () => ({ + getAppId: mockGetAppId, +})) +vi.mock('../../../util/packageManager/packageManagerChoice.js', () => ({ + getPackageManagerChoice: mockGetPackageManagerChoice, +})) +vi.mock('../../../util/packageManager/upgradePackages.js', () => ({ + upgradePackages: mockUpgradePackages, +})) +vi.mock('@sanity/cli-core', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + getLocalPackageVersion: mockGetLocalPackageVersion, + isInteractive: mockIsInteractive, + } +}) +vi.mock('@sanity/cli-core/ux', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + confirm: mockConfirm, + logSymbols: {error: '✗', info: 'ℹ', success: '✓', warning: '⚠'}, + spinner: vi.fn(() => ({ + fail: vi.fn(), + start: vi.fn(() => ({fail: vi.fn(), succeed: vi.fn()})), + succeed: vi.fn(), + })), + } +}) + +function mockServer({port = 3333}: {port?: number} = {}) { + return { + close: vi.fn().mockResolvedValue(undefined), + server: { + config: { + logger: {info: vi.fn()}, + server: {port}, + }, + }, + } +} + +describe('startStudioDevServer', () => { + beforeEach(() => { + mockCheckStudioDependencyVersions.mockResolvedValue(undefined) + mockCheckRequiredDependencies.mockResolvedValue({installedSanityVersion: '3.50.0'}) + mockShouldAutoUpdate.mockReturnValue(false) + mockGetDevServerConfig.mockReturnValue({ + basePath: '/', + cwd: '/tmp/sanity-project', + httpHost: 'localhost', + httpPort: 3333, + reactStrictMode: false, + staticPath: '/tmp/sanity-project/static', + }) + mockStartDevServer.mockResolvedValue(mockServer()) + mockGetLocalPackageVersion.mockResolvedValue('5.0.0') + mockGracefulServerDeath.mockImplementation((_cmd, _host, _port, err) => err) + mockGetAppId.mockReturnValue('app-id') + mockIsInteractive.mockReturnValue(false) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + test('starts the dev server and returns close/server', async () => { + const result = await startStudioDevServer(createDevOptions()) + + expect(mockCheckStudioDependencyVersions).toHaveBeenCalledWith( + '/tmp/sanity-project', + expect.anything(), + ) + expect(mockCheckRequiredDependencies).toHaveBeenCalled() + expect(mockStartDevServer).toHaveBeenCalled() + expect(result.close).toBeDefined() + expect(result.server).toBeDefined() + }) + + test('passes reactRefreshHost through to startDevServer', async () => { + await startStudioDevServer(createDevOptions({reactRefreshHost: 'http://localhost:3333'})) + + expect(mockStartDevServer).toHaveBeenCalledWith( + expect.objectContaining({reactRefreshHost: 'http://localhost:3333'}), + ) + }) + + test('logs schema-extraction info line when enabled in cliConfig', async () => { + const output = createMockOutput() + await startStudioDevServer( + createDevOptions({ + cliConfig: {schemaExtraction: {enabled: true}} as CliConfig, + output, + }), + ) + + expect(output.log).toHaveBeenCalledWith(expect.stringContaining('schema extraction')) + }) + + test('wraps startup failures via gracefulServerDeath', async () => { + const originalErr = Object.assign(new Error('boom'), {code: 'EADDRINUSE'}) + const wrappedErr = new Error('port in use') + mockStartDevServer.mockRejectedValueOnce(originalErr) + mockGracefulServerDeath.mockReturnValueOnce(wrappedErr) + + let error: unknown + try { + await startStudioDevServer(createDevOptions()) + } catch (err) { + error = err + } + + expect(error).toBeInstanceOf(Error) + expect(error).toBe(wrappedErr) + expect(mockGracefulServerDeath).toHaveBeenCalledWith('dev', 'localhost', 3333, originalErr) + }) + + describe('auto-updates', () => { + beforeEach(() => { + mockShouldAutoUpdate.mockReturnValue(true) + }) + + test('throws when installed sanity version cannot be parsed', async () => { + mockCheckRequiredDependencies.mockResolvedValueOnce({installedSanityVersion: 'not-a-version'}) + + let error: unknown + try { + await startStudioDevServer(createDevOptions()) + } catch (err) { + error = err + } + + expect(error).toBeInstanceOf(Error) + expect((error as Error).message).toContain('Failed to parse installed Sanity version') + }) + + test('logs info line when auto-updates enabled and versions match', async () => { + mockCompareDependencyVersions.mockResolvedValueOnce({ + mismatched: [], + unresolvedPrerelease: [], + }) + const output = createMockOutput() + + await startStudioDevServer(createDevOptions({output})) + + expect(output.log).toHaveBeenCalledWith(expect.stringContaining('auto-updates')) + expect(mockCompareDependencyVersions).toHaveBeenCalled() + }) + + test('warns for each unresolved prerelease dependency', async () => { + mockCompareDependencyVersions.mockResolvedValueOnce({ + mismatched: [], + unresolvedPrerelease: [{pkg: 'sanity', version: '3.50.0-rc.1'}], + }) + const output = createMockOutput() + + await startStudioDevServer(createDevOptions({output})) + + expect(output.warn).toHaveBeenCalledWith(expect.stringContaining('prerelease')) + }) + + test('warns when compareDependencyVersions throws', async () => { + mockCompareDependencyVersions.mockRejectedValueOnce(new Error('network down')) + const output = createMockOutput() + + await startStudioDevServer(createDevOptions({output})) + + expect(output.warn).toHaveBeenCalledWith( + expect.stringContaining('Failed to compare local versions'), + ) + }) + + test('logs mismatch message non-interactively without prompting', async () => { + mockCompareDependencyVersions.mockResolvedValueOnce({ + mismatched: [{installed: '3.49.0', pkg: 'sanity', remote: '3.50.0'}], + unresolvedPrerelease: [], + }) + mockIsInteractive.mockReturnValue(false) + const output = createMockOutput() + + await startStudioDevServer(createDevOptions({output})) + + expect(output.log).toHaveBeenCalledWith( + expect.stringContaining('different from the versions'), + ) + expect(mockConfirm).not.toHaveBeenCalled() + expect(mockUpgradePackages).not.toHaveBeenCalled() + }) + + test('prompts and upgrades packages interactively when user confirms', async () => { + mockCompareDependencyVersions.mockResolvedValueOnce({ + mismatched: [{installed: '3.49.0', pkg: 'sanity', remote: '3.50.0'}], + unresolvedPrerelease: [], + }) + mockIsInteractive.mockReturnValue(true) + mockConfirm.mockResolvedValueOnce(true) + mockGetPackageManagerChoice.mockResolvedValueOnce({chosen: 'pnpm'}) + + await startStudioDevServer(createDevOptions()) + + expect(mockConfirm).toHaveBeenCalled() + expect(mockUpgradePackages).toHaveBeenCalledWith( + expect.objectContaining({ + packageManager: 'pnpm', + packages: [['sanity', '3.50.0']], + }), + expect.anything(), + ) + }) + + test('does not upgrade when user declines the prompt', async () => { + mockCompareDependencyVersions.mockResolvedValueOnce({ + mismatched: [{installed: '3.49.0', pkg: 'sanity', remote: '3.50.0'}], + unresolvedPrerelease: [], + }) + mockIsInteractive.mockReturnValue(true) + mockConfirm.mockResolvedValueOnce(false) + + await startStudioDevServer(createDevOptions()) + + expect(mockConfirm).toHaveBeenCalled() + expect(mockUpgradePackages).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts new file mode 100644 index 000000000..5f60fd2d1 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/startWorkbenchDevServer.test.ts @@ -0,0 +1,746 @@ +import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' + +import {startWorkbenchDevServer} from '../startWorkbenchDevServer.js' +import {createDevOptions, createMockOutput} from './testHelpers.js' + +const mockResolveLocalPackage = vi.hoisted(() => vi.fn()) +const mockCreateServer = vi.hoisted(() => vi.fn()) +const mockWriteWorkbenchRuntime = vi.hoisted(() => vi.fn()) +const mockAcquireWorkbenchLock = vi.hoisted(() => vi.fn()) +const mockGetRegisteredServers = vi.hoisted(() => vi.fn()) +const mockReadWorkbenchLock = vi.hoisted(() => vi.fn()) +const mockWatchRegistry = vi.hoisted(() => vi.fn()) +const mockGetProjectById = vi.hoisted(() => vi.fn()) + +vi.mock('@sanity/cli-core', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + resolveLocalPackage: mockResolveLocalPackage, + } +}) +vi.mock('vite', () => ({createServer: mockCreateServer})) +vi.mock('@vitejs/plugin-react', () => ({default: vi.fn(() => [])})) +vi.mock('../writeWorkbenchRuntime.js', () => ({ + writeWorkbenchRuntime: mockWriteWorkbenchRuntime, +})) +vi.mock('../devServerRegistry.js', () => ({ + acquireWorkbenchLock: mockAcquireWorkbenchLock, + getRegisteredServers: mockGetRegisteredServers, + readWorkbenchLock: mockReadWorkbenchLock, + watchRegistry: mockWatchRegistry, +})) +vi.mock('../../../services/projects.js', () => ({ + getProjectById: mockGetProjectById, +})) + +function createMockServer(port = 3333) { + return { + close: vi.fn().mockResolvedValue(undefined), + config: {server: {port}}, + httpServer: {address: vi.fn().mockReturnValue({address: '127.0.0.1', family: 'IPv4', port})}, + listen: vi.fn().mockResolvedValue(undefined), + ws: {on: vi.fn(), send: vi.fn()}, + } +} + +describe('startWorkbenchDevServer', () => { + beforeEach(() => { + mockWriteWorkbenchRuntime.mockResolvedValue('/tmp/sanity-project/.sanity/workbench') + mockAcquireWorkbenchLock.mockReturnValue({release: vi.fn(), updatePort: vi.fn()}) + mockGetRegisteredServers.mockReturnValue([]) + mockReadWorkbenchLock.mockReturnValue(undefined) + mockWatchRegistry.mockReturnValue({close: vi.fn()}) + }) + + afterEach(() => { + vi.clearAllMocks() + vi.unstubAllEnvs() + }) + + describe('federation gate', () => { + test('skips workbench entirely when federation is not enabled', async () => { + const result = await startWorkbenchDevServer(createDevOptions()) + + expect(result.workbenchAvailable).toBe(false) + expect(result.close).toBeTypeOf('function') + expect(mockResolveLocalPackage).not.toHaveBeenCalled() + expect(mockCreateServer).not.toHaveBeenCalled() + }) + + test('skips workbench when federation is explicitly disabled', async () => { + const result = await startWorkbenchDevServer( + createDevOptions({cliConfig: {federation: {enabled: false}}}), + ) + + expect(result.workbenchAvailable).toBe(false) + expect(result.close).toBeTypeOf('function') + expect(mockResolveLocalPackage).not.toHaveBeenCalled() + }) + + test('returns httpHost and workbenchPort even when federation is disabled', async () => { + const result = await startWorkbenchDevServer( + createDevOptions({httpHost: '0.0.0.0', httpPort: 4000}), + ) + + expect(result.httpHost).toBe('0.0.0.0') + expect(result.workbenchPort).toBe(4000) + }) + }) + + describe('workbench availability check', () => { + test('returns workbenchAvailable: false when @sanity/workbench is not resolvable', async () => { + mockResolveLocalPackage.mockRejectedValue(new Error('Cannot find package')) + + const result = await startWorkbenchDevServer( + createDevOptions({cliConfig: {federation: {enabled: true}}}), + ) + + expect(result.workbenchAvailable).toBe(false) + expect(result.close).toBeTypeOf('function') + expect(mockCreateServer).not.toHaveBeenCalled() + }) + + test('returns httpHost and workbenchPort even when workbench is unavailable', async () => { + mockResolveLocalPackage.mockRejectedValue(new Error('Cannot find package')) + + const result = await startWorkbenchDevServer( + createDevOptions({ + cliConfig: {federation: {enabled: true}}, + httpHost: '0.0.0.0', + httpPort: 4000, + }), + ) + + expect(result.httpHost).toBe('0.0.0.0') + expect(result.workbenchPort).toBe(4000) + }) + }) + + describe('successful startup', () => { + const federationConfig = { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + } as const + + test('returns workbenchAvailable: true and close when server starts', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + const result = await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + if (!result.close) throw new Error('Expected close to be defined') + expect(result.workbenchAvailable).toBe(true) + expect(result.close).toBeDefined() + }) + + test('returns httpHost and workbenchPort from provided options', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer(4000)) + + const result = await startWorkbenchDevServer( + createDevOptions({cliConfig: federationConfig, httpHost: '0.0.0.0', httpPort: 4000}), + ) + + expect(result.httpHost).toBe('0.0.0.0') + expect(result.workbenchPort).toBe(4000) + }) + + test('returns actual port when Vite picks an alternative port', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + // Simulate Vite finding port 3333 occupied and binding to 3334 instead + const mockServer = createMockServer(3334) + mockServer.httpServer.address.mockReturnValue({ + address: '127.0.0.1', + family: 'IPv4', + port: 3334, + }) + mockCreateServer.mockResolvedValue(mockServer) + + const result = await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(result.workbenchPort).toBe(3334) + }) + + test('passes workDir to writeWorkbenchRuntime', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({cwd: '/tmp/sanity-project'}), + ) + }) + + test('passes organizationId from cliConfig.app.organizationId', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer( + createDevOptions({ + cliConfig: {app: {organizationId: 'org-123'}, federation: {enabled: true}}, + }), + ) + + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({organizationId: 'org-123'}), + ) + }) + + test('resolves organizationId from project when only api.projectId is set', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + mockGetProjectById.mockResolvedValue({organizationId: 'org-from-project'}) + + await startWorkbenchDevServer( + createDevOptions({ + cliConfig: {api: {projectId: 'proj-123'}, federation: {enabled: true}}, + }), + ) + + expect(mockGetProjectById).toHaveBeenCalledWith('proj-123') + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({organizationId: 'org-from-project'}), + ) + }) + + test('prefers cliConfig.app.organizationId over project lookup', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer( + createDevOptions({ + cliConfig: { + api: {projectId: 'proj-123'}, + app: {organizationId: 'org-explicit'}, + federation: {enabled: true}, + }, + }), + ) + + expect(mockGetProjectById).not.toHaveBeenCalled() + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({organizationId: 'org-explicit'}), + ) + }) + + test('throws when neither app.organizationId nor api.projectId is configured', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await expect( + startWorkbenchDevServer(createDevOptions({cliConfig: {federation: {enabled: true}}})), + ).rejects.toThrow(/Unable to determine organization ID/) + }) + + test('throws when project lookup returns no organizationId', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + mockGetProjectById.mockResolvedValue({organizationId: undefined}) + + await expect( + startWorkbenchDevServer( + createDevOptions({ + cliConfig: {api: {projectId: 'proj-123'}, federation: {enabled: true}}, + }), + ), + ).rejects.toThrow(/Unable to determine organization ID/) + }) + + test('configures warmup for the workbench entry file', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(mockCreateServer).toHaveBeenCalledWith( + expect.objectContaining({ + server: expect.objectContaining({ + warmup: { + clientFiles: ['./workbench.js'], + }, + }), + }), + ) + }) + }) + + describe('remote-preload Link header', () => { + const federationConfig = { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + } as const + + function getMiddleware(): (req: {url?: string}, res: ResLike, next: () => void) => void { + const calls = mockCreateServer.mock.calls + const lastCall = calls.at(-1) + if (!lastCall) throw new Error('createServer was not called') + const config = lastCall[0] as {plugins: PluginLike[]} + const plugin = config.plugins.find( + (p) => p && typeof p === 'object' && p.name === 'sanity:workbench-remote-preload-header', + ) + if (!plugin) throw new Error('remote-preload plugin not registered') + const middlewareUse = vi.fn() + plugin.configureServer?.({middlewares: {use: middlewareUse}}) + return middlewareUse.mock.calls[0][0] + } + + interface ResLike { + setHeader: (name: string, value: string) => void + } + + interface PluginLike { + configureServer?: (server: {middlewares: {use: (mw: unknown) => void}}) => void + name?: string + } + + test('does not register plugin when remoteUrl is not set', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const config = mockCreateServer.mock.calls[0][0] as {plugins: PluginLike[]} + expect( + config.plugins.find((p) => p?.name === 'sanity:workbench-remote-preload-header'), + ).toBeUndefined() + }) + + test('sets Link header on the root document', async () => { + vi.stubEnv( + 'SANITY_INTERNAL_WORKBENCH_REMOTE_URL', + 'https://workbench.example/mf-manifest.json', + ) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const middleware = getMiddleware() + const setHeader = vi.fn() + const next = vi.fn() + middleware({url: '/'}, {setHeader}, next) + + expect(setHeader).toHaveBeenCalledWith( + 'Link', + '; rel=preload; as=fetch; crossorigin', + ) + expect(next).toHaveBeenCalled() + }) + + test('sets Link header on /index.html', async () => { + vi.stubEnv( + 'SANITY_INTERNAL_WORKBENCH_REMOTE_URL', + 'https://workbench.example/mf-manifest.json', + ) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const middleware = getMiddleware() + const setHeader = vi.fn() + middleware({url: '/index.html'}, {setHeader}, vi.fn()) + + expect(setHeader).toHaveBeenCalledWith('Link', expect.stringContaining('as=fetch')) + }) + + test('ignores query strings when matching the index document', async () => { + vi.stubEnv( + 'SANITY_INTERNAL_WORKBENCH_REMOTE_URL', + 'https://workbench.example/mf-manifest.json', + ) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const middleware = getMiddleware() + const setHeader = vi.fn() + middleware({url: '/?t=1'}, {setHeader}, vi.fn()) + + expect(setHeader).toHaveBeenCalledWith('Link', expect.stringContaining('rel=preload')) + }) + + test('does not set Link header on non-document requests', async () => { + vi.stubEnv( + 'SANITY_INTERNAL_WORKBENCH_REMOTE_URL', + 'https://workbench.example/mf-manifest.json', + ) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const middleware = getMiddleware() + const setHeader = vi.fn() + const next = vi.fn() + middleware({url: '/workbench.js'}, {setHeader}, next) + + expect(setHeader).not.toHaveBeenCalled() + expect(next).toHaveBeenCalled() + }) + + test('throws when remote URL is set but invalid', async () => { + vi.stubEnv('SANITY_INTERNAL_WORKBENCH_REMOTE_URL', 'javascript:alert(1)') + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await expect( + startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})), + ).rejects.toThrow(/Invalid SANITY_INTERNAL_WORKBENCH_REMOTE_URL/) + }) + + test('accepts an http:// remote URL', async () => { + vi.stubEnv( + 'SANITY_INTERNAL_WORKBENCH_REMOTE_URL', + 'http://workbench.example/mf-manifest.json', + ) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const middleware = getMiddleware() + const setHeader = vi.fn() + middleware({url: '/'}, {setHeader}, vi.fn()) + + expect(setHeader).toHaveBeenCalledWith( + 'Link', + '; rel=preload; as=fetch; crossorigin', + ) + }) + }) + + describe('reactStrictMode', () => { + test('uses SANITY_STUDIO_REACT_STRICT_MODE=true env var over cliConfig', async () => { + vi.stubEnv('SANITY_STUDIO_REACT_STRICT_MODE', 'true') + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer( + createDevOptions({ + cliConfig: { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + reactStrictMode: false, + }, + }), + ) + + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({reactStrictMode: true}), + ) + }) + + test('uses SANITY_STUDIO_REACT_STRICT_MODE=false env var over cliConfig', async () => { + vi.stubEnv('SANITY_STUDIO_REACT_STRICT_MODE', 'false') + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer( + createDevOptions({ + cliConfig: { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + reactStrictMode: true, + }, + }), + ) + + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({reactStrictMode: false}), + ) + }) + + test('falls back to cliConfig.reactStrictMode when env var is not set', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer( + createDevOptions({ + cliConfig: { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + reactStrictMode: true, + }, + }), + ) + + expect(mockWriteWorkbenchRuntime).toHaveBeenCalledWith( + expect.objectContaining({reactStrictMode: true}), + ) + }) + }) + + describe('server startup failure', () => { + const federationConfig = { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + } as const + + test('warns and returns without close when listen() throws', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockServer.listen.mockRejectedValue(new Error('Port already in use')) + mockCreateServer.mockResolvedValue(mockServer) + const output = createMockOutput() + + const result = await startWorkbenchDevServer( + createDevOptions({cliConfig: federationConfig, output}), + ) + + expect(result.workbenchAvailable).toBe(false) + expect(result.close).toBeTypeOf('function') + expect(output.warn).toHaveBeenCalledWith(expect.stringContaining('Port already in use')) + }) + + test('closes the server before returning when listen() throws', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockServer.listen.mockRejectedValue(new Error('Port already in use')) + mockCreateServer.mockResolvedValue(mockServer) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(mockServer.close).toHaveBeenCalled() + }) + }) + + describe('singleton detection', () => { + const federationConfig = { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + } as const + + test('skips starting server when lock is held by another process', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockAcquireWorkbenchLock.mockReturnValue(undefined) + mockReadWorkbenchLock.mockReturnValue({host: '0.0.0.0', pid: 12_345, port: 4000}) + + const result = await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(result.workbenchAvailable).toBe(true) + expect(result.workbenchPort).toBe(4000) + expect(result.httpHost).toBe('0.0.0.0') + expect(result.close).toBeTypeOf('function') + expect(mockCreateServer).not.toHaveBeenCalled() + }) + + test('falls back to configured host/port when lock is held but lock file unreadable', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockAcquireWorkbenchLock.mockReturnValue(undefined) + mockReadWorkbenchLock.mockReturnValue(undefined) + + const result = await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(result.workbenchAvailable).toBe(true) + expect(result.workbenchPort).toBe(3333) + expect(result.httpHost).toBe('localhost') + expect(mockCreateServer).not.toHaveBeenCalled() + }) + }) + + describe('registry integration', () => { + const federationConfig = { + app: {organizationId: 'org-test'}, + federation: {enabled: true}, + } as const + + test('updates lock with actual port after successful startup', async () => { + const mockUpdatePort = vi.fn() + mockAcquireWorkbenchLock.mockReturnValue({release: vi.fn(), updatePort: mockUpdatePort}) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer(3334)) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(mockUpdatePort).toHaveBeenCalledWith(3334) + }) + + test('starts watching registry after successful startup', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(mockWatchRegistry).toHaveBeenCalledWith(expect.any(Function)) + }) + + test('watcher callback broadcasts applications via server.ws.send with inlined manifests', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockCreateServer.mockResolvedValue(mockServer) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const studioManifest = {createdAt: '2026-01-01T00:00:00.000Z', version: 3, workspaces: []} + const appManifest = {icon: 'two', title: 'App Two', version: '1'} + + const watchCallback = mockWatchRegistry.mock.calls[0][0] + watchCallback([ + { + host: 'localhost', + id: 'app-1', + manifest: studioManifest, + pid: 2, + port: 3334, + type: 'studio', + }, + { + host: 'localhost', + id: 'app-2', + manifest: appManifest, + pid: 3, + port: 3335, + type: 'coreApp', + }, + ]) + + expect(mockServer.ws.send).toHaveBeenCalledWith('sanity:workbench:local-applications', { + applications: [ + { + host: 'localhost', + id: 'app-1', + manifest: studioManifest, + port: 3334, + type: 'studio', + }, + { + host: 'localhost', + id: 'app-2', + manifest: appManifest, + port: 3335, + type: 'coreApp', + }, + ], + }) + }) + + test('includes undefined manifest when a registered server has not yet extracted one', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockCreateServer.mockResolvedValue(mockServer) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const watchCallback = mockWatchRegistry.mock.calls[0][0] + watchCallback([{host: 'localhost', pid: 2, port: 3334, type: 'studio'}]) + + expect(mockServer.ws.send).toHaveBeenCalledWith('sanity:workbench:local-applications', { + applications: [ + { + host: 'localhost', + id: undefined, + manifest: undefined, + port: 3334, + type: 'studio', + }, + ], + }) + }) + + test('forwards projectId from registry entries through the broadcast payload', async () => { + // Workbench needs the projectId on the very first event to resolve a + // local studio's primary project before the manifest arrives. + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockCreateServer.mockResolvedValue(mockServer) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const watchCallback = mockWatchRegistry.mock.calls[0][0] + watchCallback([ + { + host: 'localhost', + id: 'app-1', + pid: 2, + port: 3334, + projectId: 'x1g7jygt', + type: 'studio', + }, + ]) + + expect(mockServer.ws.send).toHaveBeenCalledWith('sanity:workbench:local-applications', { + applications: [ + expect.objectContaining({ + host: 'localhost', + id: 'app-1', + port: 3334, + projectId: 'x1g7jygt', + type: 'studio', + }), + ], + }) + }) + + test('responds to client request with current applications', async () => { + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockCreateServer.mockResolvedValue(mockServer) + const inlined = {icon: 'inline', title: 'Title', version: '1'} + mockGetRegisteredServers.mockReturnValue([ + { + host: 'localhost', + id: 'app-1', + manifest: inlined, + pid: 2, + port: 3334, + type: 'coreApp', + }, + ]) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + const onCall = mockServer.ws.on.mock.calls.find( + (args: unknown[]) => args[0] === 'sanity:workbench:get-local-applications', + ) + expect(onCall).toBeDefined() + + const mockClient = {send: vi.fn()} + const handler = onCall![1] as (data: unknown, client: typeof mockClient) => void + handler(undefined, mockClient) + + expect(mockClient.send).toHaveBeenCalledWith('sanity:workbench:local-applications', { + applications: [ + { + host: 'localhost', + id: 'app-1', + manifest: inlined, + port: 3334, + type: 'coreApp', + }, + ], + }) + }) + + test('close stops watcher and releases lock', async () => { + const mockReleaseLock = vi.fn() + const mockWatcherClose = vi.fn() + mockAcquireWorkbenchLock.mockReturnValue({release: mockReleaseLock, updatePort: vi.fn()}) + mockWatchRegistry.mockReturnValue({close: mockWatcherClose}) + mockResolveLocalPackage.mockResolvedValue({}) + mockCreateServer.mockResolvedValue(createMockServer()) + + const result = await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + await result.close() + + expect(mockWatcherClose).toHaveBeenCalled() + expect(mockReleaseLock).toHaveBeenCalled() + }) + + test('releases lock when server startup fails', async () => { + const mockReleaseLock = vi.fn() + mockAcquireWorkbenchLock.mockReturnValue({release: mockReleaseLock, updatePort: vi.fn()}) + mockResolveLocalPackage.mockResolvedValue({}) + const mockServer = createMockServer() + mockServer.listen.mockRejectedValue(new Error('Port already in use')) + mockCreateServer.mockResolvedValue(mockServer) + + await startWorkbenchDevServer(createDevOptions({cliConfig: federationConfig})) + + expect(mockReleaseLock).toHaveBeenCalled() + }) + }) +}) diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts b/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts new file mode 100644 index 000000000..dcdcba694 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/testHelpers.ts @@ -0,0 +1,51 @@ +import {type CliConfig, type Output} from '@sanity/cli-core' +// eslint-disable-next-line import-x/no-extraneous-dependencies +import {vi} from 'vitest' + +import {type StartWorkbenchOptions} from '../startWorkbenchDevServer.js' +import {type DevActionOptions} from '../types.js' + +/** Shared test helpers for dev-action test suites. */ + +export function createMockOutput(): Output { + return { + error: vi.fn(), + log: vi.fn(), + warn: vi.fn(), + } as unknown as Output +} + +/** Minimal flags object accepted by the dev command — values aren't asserted + * by the code under test but are required to type-check as `DevFlags`. */ +const DEV_FLAGS = { + 'auto-updates': false, + host: 'localhost', + json: false, + port: '3333', +} as const + +export function createDevOptions( + overrides: Partial = {}, +): StartWorkbenchOptions { + return { + cliConfig: {} as CliConfig, + flags: DEV_FLAGS, + httpHost: 'localhost', + httpPort: 3333, + isApp: false, + output: createMockOutput(), + workDir: '/tmp/sanity-project', + ...overrides, + } +} + +export function createBaseDevOptions(overrides: Partial = {}): DevActionOptions { + return { + cliConfig: {} as CliConfig, + flags: DEV_FLAGS, + isApp: false, + output: createMockOutput(), + workDir: '/tmp/sanity-project', + ...overrides, + } +} diff --git a/packages/@sanity/cli/src/actions/dev/__tests__/writeWorkbenchRuntime.test.ts b/packages/@sanity/cli/src/actions/dev/__tests__/writeWorkbenchRuntime.test.ts new file mode 100644 index 000000000..af0f0eda1 --- /dev/null +++ b/packages/@sanity/cli/src/actions/dev/__tests__/writeWorkbenchRuntime.test.ts @@ -0,0 +1,205 @@ +import {mkdtempSync, rmSync} from 'node:fs' +import fs from 'node:fs/promises' +import {tmpdir} from 'node:os' +import {join} from 'node:path' + +import {afterEach, beforeEach, describe, expect, test} from 'vitest' + +import {writeWorkbenchRuntime} from '../writeWorkbenchRuntime.js' + +describe('writeWorkbenchRuntime', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'sanity-workbench-')) + }) + + afterEach(() => { + rmSync(tmpDir, {recursive: true}) + }) + + test('returns the absolute path to the workbench directory', async () => { + const result = await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + expect(result).toBe(join(tmpDir, '.sanity', 'workbench')) + }) + + test('creates the .sanity/workbench directory', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const stat = await fs.stat(join(tmpDir, '.sanity', 'workbench')) + expect(stat.isDirectory()).toBe(true) + }) + + test('creates nested directories even if .sanity does not exist', async () => { + const nestedCwd = join(tmpDir, 'nested', 'project') + await fs.mkdir(nestedCwd, {recursive: true}) + + await writeWorkbenchRuntime({cwd: nestedCwd, reactStrictMode: false}) + + const stat = await fs.stat(join(nestedCwd, '.sanity', 'workbench')) + expect(stat.isDirectory()).toBe(true) + }) + + describe('workbench.js', () => { + test('writes workbench.js to the workbench directory', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const stat = await fs.stat(join(tmpDir, '.sanity', 'workbench', 'workbench.js')) + expect(stat.isFile()).toBe(true) + }) + + test('imports renderWorkbench from sanity/workbench', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const content = await fs.readFile( + join(tmpDir, '.sanity', 'workbench', 'workbench.js'), + 'utf8', + ) + expect(content).toContain('import {renderWorkbench} from "sanity/workbench"') + }) + + test('substitutes reactStrictMode: false into workbench.js', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const content = await fs.readFile( + join(tmpDir, '.sanity', 'workbench', 'workbench.js'), + 'utf8', + ) + expect(content).toContain('{reactStrictMode: false}') + expect(content).not.toContain('%SANITY_WORKBENCH_REACT_STRICT_MODE%') + }) + + test('substitutes reactStrictMode: true into workbench.js', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: true}) + + const content = await fs.readFile( + join(tmpDir, '.sanity', 'workbench', 'workbench.js'), + 'utf8', + ) + expect(content).toContain('{reactStrictMode: true}') + expect(content).not.toContain('%SANITY_WORKBENCH_REACT_STRICT_MODE%') + }) + + test('passes the workbench element id to renderWorkbench', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const content = await fs.readFile( + join(tmpDir, '.sanity', 'workbench', 'workbench.js'), + 'utf8', + ) + expect(content).toContain('document.getElementById("workbench")') + }) + + test('passes organizationId: undefined when not provided', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const content = await fs.readFile( + join(tmpDir, '.sanity', 'workbench', 'workbench.js'), + 'utf8', + ) + expect(content).toContain('{organizationId: undefined}') + expect(content).not.toContain('%SANITY_WORKBENCH_ORGANIZATION_ID%') + }) + + test('passes organizationId as string when provided', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, organizationId: 'org-123', reactStrictMode: false}) + + const content = await fs.readFile( + join(tmpDir, '.sanity', 'workbench', 'workbench.js'), + 'utf8', + ) + expect(content).toContain('{organizationId: "org-123"}') + expect(content).not.toContain('%SANITY_WORKBENCH_ORGANIZATION_ID%') + }) + }) + + describe('index.html', () => { + test('writes index.html to the workbench directory', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const stat = await fs.stat(join(tmpDir, '.sanity', 'workbench', 'index.html')) + expect(stat.isFile()).toBe(true) + }) + + test('includes a div with id="workbench"', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const content = await fs.readFile(join(tmpDir, '.sanity', 'workbench', 'index.html'), 'utf8') + expect(content).toContain('
') + }) + + test('includes a module script tag loading workbench.js', async () => { + await writeWorkbenchRuntime({cwd: tmpDir, reactStrictMode: false}) + + const content = await fs.readFile(join(tmpDir, '.sanity', 'workbench', 'index.html'), 'utf8') + expect(content).toContain(' + + +` + +/** + * Generates the `.sanity/workbench` directory with static entry files for + * the workbench Vite dev server. + * + * @param cwd - Current working directory (Sanity root dir) + * @returns The absolute path to the written workbench runtime directory + * @internal + */ +export async function writeWorkbenchRuntime(options: { + cwd: string + organizationId?: string + reactStrictMode: boolean + remoteUrl?: string +}): Promise { + const {cwd, organizationId, reactStrictMode, remoteUrl} = options + const workbenchDir = path.join(cwd, '.sanity', 'workbench') + + const workbenchJs = workbenchJsTemplate + .replace( + /%SANITY_WORKBENCH_ORGANIZATION_ID%/, + organizationId === undefined ? 'undefined' : JSON.stringify(organizationId), + ) + .replace(/%SANITY_WORKBENCH_REACT_STRICT_MODE%/, JSON.stringify(reactStrictMode)) + + const prefetchHints = buildPrefetchHints(remoteUrl) + + const indexHtml = indexHtmlTemplate.replace(/%SANITY_WORKBENCH_PREFETCH_HINTS%/, prefetchHints) + + devDebug('Making workbench runtime directory') + await fs.mkdir(workbenchDir, {recursive: true}) + + devDebug('Writing workbench.js to workbench runtime directory') + await fs.writeFile(path.join(workbenchDir, 'workbench.js'), workbenchJs) + + devDebug('Writing index.html to workbench runtime directory') + await fs.writeFile(path.join(workbenchDir, 'index.html'), indexHtml) + + return workbenchDir +} + +function buildPrefetchHints(remoteUrl: string | undefined): string { + if (!remoteUrl) return '' + + try { + const url = new URL(remoteUrl) + return [ + ` `, + ` `, + ].join('\n') + } catch { + return '' + } +} diff --git a/packages/@sanity/cli/src/actions/doctor/__tests__/cliInstallationCheck.test.ts b/packages/@sanity/cli/src/actions/doctor/__tests__/cliInstallationCheck.test.ts index 9908b2290..0924abfb5 100644 --- a/packages/@sanity/cli/src/actions/doctor/__tests__/cliInstallationCheck.test.ts +++ b/packages/@sanity/cli/src/actions/doctor/__tests__/cliInstallationCheck.test.ts @@ -5,6 +5,12 @@ import {afterEach, describe, expect, test, vi} from 'vitest' import {cliInstallationCheck} from '../checks/cliInstallation.js' +// Prevent real global CLI installations on the developer's machine from +// leaking into tests and producing environment-dependent warnings +vi.mock('../../../util/packageManager/installationInfo/detectGlobals.js', () => ({ + detectGlobalInstallations: vi.fn().mockResolvedValue([]), +})) + const __dirname = path.dirname(fileURLToPath(import.meta.url)) const fixturesDir = path.join( __dirname, diff --git a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts index bb1ba9530..e8fa3a768 100644 --- a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts +++ b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapLocalTemplate.test.ts @@ -5,6 +5,7 @@ import path from 'node:path' import {type Output} from '@sanity/cli-core' import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' +import {resolveLatestVersions} from '../../../util/resolveLatestVersions.js' import {bootstrapLocalTemplate} from '../bootstrapLocalTemplate.js' vi.mock('../../../util/resolveLatestVersions.js', () => ({ @@ -52,6 +53,7 @@ describe('bootstrapLocalTemplate (app templates)', () => { variables: { autoUpdates: false, dataset: 'production', + federation: false, organizationId: 'org1', projectId: 'abc123', projectName: 'my-app', @@ -75,6 +77,7 @@ describe('bootstrapLocalTemplate (app templates)', () => { variables: { autoUpdates: false, dataset: '', + federation: false, organizationId: 'org1', projectId: '', projectName: 'my-app', @@ -88,3 +91,86 @@ describe('bootstrapLocalTemplate (app templates)', () => { expect(appTsx).not.toContain('%dataset%') }) }) + +describe('bootstrapLocalTemplate (federation)', () => { + let tmp: string + beforeEach(async () => { + tmp = await mkdtemp(path.join(tmpdir(), 'cli-bootstrap-')) + }) + afterEach(async () => { + await rm(tmp, {force: true, recursive: true}) + vi.clearAllMocks() + }) + + test('overrides the `sanity` dependency with the `workbench` dist-tag when federation is enabled', async () => { + await bootstrapLocalTemplate({ + output: makeOutput(), + outputPath: tmp, + packageName: 'my-studio', + templateName: 'clean', + useTypeScript: true, + variables: { + autoUpdates: false, + dataset: 'production', + federation: true, + organizationId: 'org1', + projectId: 'abc123', + projectName: 'my-studio', + }, + }) + + expect(resolveLatestVersions).toHaveBeenCalledOnce() + const resolvedDeps = vi.mocked(resolveLatestVersions).mock.calls[0][0] + expect(resolvedDeps.sanity).toBe('workbench') + + const pkgJson = JSON.parse(await readFile(path.join(tmp, 'package.json'), 'utf8')) + expect(pkgJson.dependencies.sanity).toBe('1.0.0') + + const gitignore = await readFile(path.join(tmp, '.gitignore'), 'utf8') + expect(gitignore).toContain('.__mf__temp/') + }) + + test('keeps the `sanity` dependency on the `latest` dist-tag when federation is disabled', async () => { + await bootstrapLocalTemplate({ + output: makeOutput(), + outputPath: tmp, + packageName: 'my-studio', + templateName: 'clean', + useTypeScript: true, + variables: { + autoUpdates: false, + dataset: 'production', + federation: false, + organizationId: 'org1', + projectId: 'abc123', + projectName: 'my-studio', + }, + }) + + expect(resolveLatestVersions).toHaveBeenCalledOnce() + const resolvedDeps = vi.mocked(resolveLatestVersions).mock.calls[0][0] + expect(resolvedDeps.sanity).toBe('latest') + }) + + test('overrides the `sanity` devDependency for app templates when federation is enabled', async () => { + await bootstrapLocalTemplate({ + output: makeOutput(), + outputPath: tmp, + packageName: 'my-app', + templateName: 'app-quickstart', + useTypeScript: true, + variables: { + autoUpdates: false, + dataset: 'production', + federation: true, + organizationId: 'org1', + projectId: 'abc123', + projectName: 'my-app', + }, + }) + + expect(resolveLatestVersions).toHaveBeenCalledOnce() + const resolvedDeps = vi.mocked(resolveLatestVersions).mock.calls[0][0] + expect(resolvedDeps.sanity).toBe('workbench') + }) +}) diff --git a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts index ec40817bd..974b0df4d 100644 --- a/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts +++ b/packages/@sanity/cli/src/actions/init/__tests__/bootstrapRemoteTemplate.test.ts @@ -76,6 +76,7 @@ const baseOpts = { variables: { autoUpdates: false, dataset: 'production', + federation: true, projectId: 'test-project-id', }, } diff --git a/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts b/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts index 79caf3863..773d39829 100644 --- a/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts +++ b/packages/@sanity/cli/src/actions/init/bootstrapLocalTemplate.ts @@ -89,6 +89,7 @@ export async function bootstrapLocalTemplate( ...(isAppTemplate ? sdkAppDependencies.devDependencies : studioDependencies.devDependencies), ...template.dependencies, ...template.devDependencies, + ...(variables.federation && {sanity: 'workbench'}), }) spin.succeed() @@ -141,11 +142,13 @@ export async function bootstrapLocalTemplate( const cliConfig = isAppTemplate ? createAppCliConfig({ entry: template.entry!, + federation: variables.federation, organizationId: variables.organizationId, }) : createCliConfig({ autoUpdates: variables.autoUpdates, dataset: variables.dataset, + federation: variables.federation, projectId: variables.projectId, }) diff --git a/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts b/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts index a356535ea..73d37a347 100644 --- a/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts +++ b/packages/@sanity/cli/src/actions/init/bootstrapTemplate.ts @@ -9,6 +9,7 @@ interface BootstrapTemplateOptions { autoUpdates: boolean bearerToken: string | undefined dataset: string + federation: boolean organizationId: string | undefined output: Output outputPath: string @@ -27,6 +28,7 @@ export async function bootstrapTemplate({ autoUpdates, bearerToken, dataset, + federation, organizationId, output, outputPath, @@ -41,6 +43,7 @@ export async function bootstrapTemplate({ const bootstrapVariables: GenerateConfigOptions['variables'] = { autoUpdates, dataset, + federation, organizationId, projectId, projectName, diff --git a/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts b/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts index cfffa0df7..185b3b9d8 100644 --- a/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts +++ b/packages/@sanity/cli/src/actions/init/createAppCliConfig.ts @@ -8,17 +8,22 @@ export default defineCliConfig({ organizationId: '%organizationId%', entry: '%entry%', }, + federation: { + enabled: __BOOL__federation__, + }, }) ` interface GenerateCliConfigOptions { entry: string + federation: boolean organizationId?: string } export function createAppCliConfig(options: GenerateCliConfigOptions): string { return processTemplate({ + includeBooleanTransform: true, template: defaultAppTemplate, variables: options, }) diff --git a/packages/@sanity/cli/src/actions/init/createCliConfig.ts b/packages/@sanity/cli/src/actions/init/createCliConfig.ts index 42f7c1544..167aff579 100644 --- a/packages/@sanity/cli/src/actions/init/createCliConfig.ts +++ b/packages/@sanity/cli/src/actions/init/createCliConfig.ts @@ -14,13 +14,17 @@ export default defineCliConfig({ * Learn more at https://www.sanity.io/docs/studio/latest-version-of-sanity#k47faf43faf56 */ autoUpdates: __BOOL__autoUpdates__, - } + }, + federation: { + enabled: __BOOL__federation__, + }, }) ` interface GenerateCliConfigOptions { autoUpdates: boolean dataset: string + federation: boolean projectId: string } diff --git a/packages/@sanity/cli/src/actions/init/createStudioConfig.ts b/packages/@sanity/cli/src/actions/init/createStudioConfig.ts index 67a191466..242656a9d 100644 --- a/packages/@sanity/cli/src/actions/init/createStudioConfig.ts +++ b/packages/@sanity/cli/src/actions/init/createStudioConfig.ts @@ -31,6 +31,7 @@ export interface GenerateConfigOptions { variables: { autoUpdates: boolean dataset: string + federation: boolean organizationId?: string projectId: string projectName?: string diff --git a/packages/@sanity/cli/src/actions/init/initAction.ts b/packages/@sanity/cli/src/actions/init/initAction.ts index a1fd41b74..f3b3ea176 100644 --- a/packages/@sanity/cli/src/actions/init/initAction.ts +++ b/packages/@sanity/cli/src/actions/init/initAction.ts @@ -6,6 +6,7 @@ import {type TelemetryTrace} from '@sanity/telemetry' import {type Framework, frameworks} from '@vercel/frameworks' import deburr from 'lodash-es/deburr.js' +import {promptForFederation} from '../../prompts/init/federation.js' import {promptForConfigFiles} from '../../prompts/init/nextjs.js' import {getCliUser} from '../../services/user.js' import {CLIInitStepCompleted, type InitStepResult} from '../../telemetry/init.telemetry.js' @@ -250,8 +251,14 @@ export async function initAction(options: InitOptions, context: InitContext): Pr return } + let federation = flagOrDefault(options.federation, true) + if (shouldPrompt(options.unattended, options.federation)) { + federation = await promptForFederation() + } + const sharedParams = { defaults, + federation, mcpConfigured, options, organizationId, diff --git a/packages/@sanity/cli/src/actions/init/initApp.ts b/packages/@sanity/cli/src/actions/init/initApp.ts index db4f2dd1c..188e3cb14 100644 --- a/packages/@sanity/cli/src/actions/init/initApp.ts +++ b/packages/@sanity/cli/src/actions/init/initApp.ts @@ -15,6 +15,7 @@ import {type InitOptions} from './types.js' export async function initApp({ datasetName, defaults, + federation, mcpConfigured, options, organizationId, @@ -28,6 +29,7 @@ export async function initApp({ }: { datasetName: string defaults: {projectName: string} + federation: boolean mcpConfigured: EditorName[] options: InitOptions organizationId: string | undefined @@ -57,6 +59,7 @@ export async function initApp({ datasetName, defaults, displayName: '', + federation, options, organizationId, output, diff --git a/packages/@sanity/cli/src/actions/init/initStudio.ts b/packages/@sanity/cli/src/actions/init/initStudio.ts index a10d0c1a3..ecbcee64d 100644 --- a/packages/@sanity/cli/src/actions/init/initStudio.ts +++ b/packages/@sanity/cli/src/actions/init/initStudio.ts @@ -27,6 +27,7 @@ export async function initStudio({ datasetName, defaults, displayName, + federation, isFirstProject, mcpConfigured, options, @@ -42,6 +43,7 @@ export async function initStudio({ datasetName: string defaults: {projectName: string} displayName: string + federation: boolean isFirstProject: boolean mcpConfigured: EditorName[] options: InitOptions @@ -91,6 +93,7 @@ export async function initStudio({ datasetName, defaults, displayName, + federation, options, organizationId, output, diff --git a/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts b/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts index d580ccfb2..c213ae8b8 100644 --- a/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts +++ b/packages/@sanity/cli/src/actions/init/scaffoldTemplate.ts @@ -92,6 +92,7 @@ export async function scaffoldAndInstall({ datasetName, defaults, displayName, + federation, options, organizationId, output, @@ -107,6 +108,7 @@ export async function scaffoldAndInstall({ datasetName: string defaults: {projectName: string} displayName: string + federation: boolean options: InitOptions organizationId: string | undefined output: Output @@ -126,6 +128,7 @@ export async function scaffoldAndInstall({ autoUpdates, bearerToken: templateToken, dataset: datasetName, + federation, organizationId, output, outputPath, diff --git a/packages/@sanity/cli/src/actions/init/types.ts b/packages/@sanity/cli/src/actions/init/types.ts index 5a92e8ed9..72cd0d68c 100644 --- a/packages/@sanity/cli/src/actions/init/types.ts +++ b/packages/@sanity/cli/src/actions/init/types.ts @@ -31,6 +31,7 @@ export interface InitOptions { coupon?: string dataset?: string env?: string + federation?: boolean git?: boolean | string importDataset?: boolean nextjsAddConfigFiles?: boolean @@ -69,6 +70,7 @@ interface InitCommandFlags { 'create-project'?: string dataset?: string env?: string + federation?: boolean git?: string 'import-dataset'?: boolean 'nextjs-add-config-files'?: boolean @@ -121,6 +123,7 @@ export function flagsToInitOptions( dataset: flags.dataset, datasetDefault: flags['dataset-default'], env: flags.env, + federation: flags.federation, fromCreate: flags['from-create'], git: flags['no-git'] ? false : flags.git, importDataset: flags['import-dataset'], diff --git a/packages/@sanity/cli/src/actions/manifest/__tests__/extractAppManifest.test.ts b/packages/@sanity/cli/src/actions/manifest/__tests__/extractCoreAppManifest.test.ts similarity index 79% rename from packages/@sanity/cli/src/actions/manifest/__tests__/extractAppManifest.test.ts rename to packages/@sanity/cli/src/actions/manifest/__tests__/extractCoreAppManifest.test.ts index bf352a3ad..2f1811e63 100644 --- a/packages/@sanity/cli/src/actions/manifest/__tests__/extractAppManifest.test.ts +++ b/packages/@sanity/cli/src/actions/manifest/__tests__/extractCoreAppManifest.test.ts @@ -1,15 +1,15 @@ import {readFile} from 'node:fs/promises' -import {getCliConfig} from '@sanity/cli-core' +import {getCliConfigUncached} from '@sanity/cli-core' import {afterEach, describe, expect, test, vi} from 'vitest' -import {extractAppManifest} from '../extractAppManifest.js' +import {extractCoreAppManifest} from '../extractCoreAppManifest.js' vi.mock('@sanity/cli-core', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - getCliConfig: vi.fn(), + getCliConfigUncached: vi.fn(), } }) @@ -25,17 +25,17 @@ vi.mock('@sanity/cli-core/ux', async (importOriginal) => { } }) -const mockGetCliConfig = vi.mocked(getCliConfig) +const mockGetCliConfig = vi.mocked(getCliConfigUncached) const mockReadFile = vi.mocked(readFile) -describe('extractAppManifest', () => { +describe('extractCoreAppManifest', () => { afterEach(() => { vi.clearAllMocks() }) test('returns undefined when no app config', async () => { mockGetCliConfig.mockResolvedValue({app: undefined} as never) - const result = await extractAppManifest({workDir: '/project'}) + const result = await extractCoreAppManifest({workDir: '/project'}) expect(result).toBeUndefined() }) @@ -44,7 +44,7 @@ describe('extractAppManifest', () => { app: {organizationId: 'org-1', title: 'My App'}, } as never) - const result = await extractAppManifest({workDir: '/project'}) + const result = await extractCoreAppManifest({workDir: '/project'}) expect(result).toEqual({title: 'My App', version: '1'}) expect(mockReadFile).not.toHaveBeenCalled() @@ -57,7 +57,7 @@ describe('extractAppManifest', () => { } as never) mockReadFile.mockResolvedValue('') - const result = await extractAppManifest({workDir}) + const result = await extractCoreAppManifest({workDir}) expect(mockReadFile).toHaveBeenCalledWith(expect.stringContaining('icon.svg'), 'utf8') expect(result?.icon).toMatch(/]/i) @@ -70,7 +70,7 @@ describe('extractAppManifest', () => { app: {icon: '../../../etc/passwd', organizationId: 'org-1'}, } as never) - await expect(extractAppManifest({workDir: '/project'})).rejects.toThrow( + await expect(extractCoreAppManifest({workDir: '/project'})).rejects.toThrow( /resolves outside the project directory/, ) @@ -83,7 +83,7 @@ describe('extractAppManifest', () => { } as never) mockReadFile.mockResolvedValue('hello world') - await expect(extractAppManifest({workDir: '/project'})).rejects.toThrow( + await expect(extractCoreAppManifest({workDir: '/project'})).rejects.toThrow( /does not contain an SVG element/, ) }) @@ -94,7 +94,7 @@ describe('extractAppManifest', () => { } as never) mockReadFile.mockRejectedValue(new Error('ENOENT: no such file or directory')) - await expect(extractAppManifest({workDir: '/project'})).rejects.toThrow( + await expect(extractCoreAppManifest({workDir: '/project'})).rejects.toThrow( /Could not read icon file at "missing.svg"/, ) }) diff --git a/packages/@sanity/cli/src/actions/manifest/extractAppManifest.ts b/packages/@sanity/cli/src/actions/manifest/extractCoreAppManifest.ts similarity index 91% rename from packages/@sanity/cli/src/actions/manifest/extractAppManifest.ts rename to packages/@sanity/cli/src/actions/manifest/extractCoreAppManifest.ts index 3f6cae5d5..0ed9400b7 100644 --- a/packages/@sanity/cli/src/actions/manifest/extractAppManifest.ts +++ b/packages/@sanity/cli/src/actions/manifest/extractCoreAppManifest.ts @@ -1,13 +1,13 @@ import {readFile} from 'node:fs/promises' import {relative, resolve} from 'node:path' -import {getCliConfig} from '@sanity/cli-core' +import {getCliConfigUncached} from '@sanity/cli-core' import {spinner} from '@sanity/cli-core/ux' import {getErrorMessage} from '../../util/getErrorMessage.js' import {type CoreAppManifest, coreAppManifestSchema} from './types.js' -interface ExtractAppManifestOptions { +interface ExtractCoreAppManifestOptions { workDir: string } @@ -52,11 +52,11 @@ async function readIconFromPath(workDir: string, iconPath: string): Promise { const {workDir} = options - const {app} = await getCliConfig(workDir) + const {app} = await getCliConfigUncached(workDir) if (!app) { return undefined } diff --git a/packages/@sanity/cli/src/actions/manifest/extractManifest.ts b/packages/@sanity/cli/src/actions/manifest/extractManifest.ts index e38ec2a11..892315622 100644 --- a/packages/@sanity/cli/src/actions/manifest/extractManifest.ts +++ b/packages/@sanity/cli/src/actions/manifest/extractManifest.ts @@ -1,11 +1,11 @@ -import {findProjectRoot, getTimer, studioWorkerTask} from '@sanity/cli-core' +import {getTimer, studioWorkerTask} from '@sanity/cli-core' import {spinner} from '@sanity/cli-core/ux' import {type ExtractSchemaWorkerError} from '../schema/types.js' import {SchemaExtractionError} from '../schema/utils/SchemaExtractionError.js' import {manifestDebug} from './debug.js' import {type CreateWorkspaceManifest, type ExtractManifestWorkerData} from './types' -import {writeManifestFile} from './writeManifestFile.js' +import {writeManifestFile, type WriteManifestFileOptions} from './writeManifestFile.js' const CREATE_TIMER = 'create-manifest' @@ -16,13 +16,17 @@ interface ExtractManifestWorkerResult { type ExtractManifestWorkerMessage = ExtractManifestWorkerResult | ExtractSchemaWorkerError -export async function extractManifest(outPath: string): Promise { - const projectRoot = await findProjectRoot(process.cwd()) - - manifestDebug('Project root %o', projectRoot) +interface ExtractManifestOptions extends Pick { + /** Absolute path to the studio's `sanity.config.(ts|js)` entry file. */ + path: string +} - const workDir = projectRoot.directory - const configPath = projectRoot.path +export async function extractManifest({ + outPath, + path, + workDir, +}: ExtractManifestOptions): Promise { + manifestDebug('Project root %o', {directory: workDir, path}) const timer = getTimer() timer.start(CREATE_TIMER) @@ -34,7 +38,7 @@ export async function extractManifest(outPath: string): Promise { { name: 'extractManifest', studioRootPath: workDir, - workerData: {configPath, workDir} satisfies ExtractManifestWorkerData, + workerData: {configPath: path, workDir} satisfies ExtractManifestWorkerData, }, ) diff --git a/packages/@sanity/cli/src/actions/manifest/types.ts b/packages/@sanity/cli/src/actions/manifest/types.ts index 6c71e7e3c..4fe95e0ec 100644 --- a/packages/@sanity/cli/src/actions/manifest/types.ts +++ b/packages/@sanity/cli/src/actions/manifest/types.ts @@ -32,6 +32,16 @@ export const coreAppManifestSchema = z.object({ export type CoreAppManifest = z.infer +/** + * Studio application manifest (serialized `create-manifest.json`). Kept + * loose so the CLI isn't coupled to the workbench's evolving client-side + * schema — the workbench consumer is authoritative on the inner shape. + * See its `ClientManifest` for the fields clients expect. + */ +export const studioManifestSchema = z.record(z.string(), z.unknown()) + +export type StudioManifest = z.infer + export interface ManifestWorkspaceFile extends Omit { schema: string // filename tools: string // filename diff --git a/packages/@sanity/cli/src/actions/manifest/writeManifestFile.ts b/packages/@sanity/cli/src/actions/manifest/writeManifestFile.ts index 15b79dba4..0e57fb9b6 100644 --- a/packages/@sanity/cli/src/actions/manifest/writeManifestFile.ts +++ b/packages/@sanity/cli/src/actions/manifest/writeManifestFile.ts @@ -6,19 +6,30 @@ import {getLocalPackageVersion, subdebug} from '@sanity/cli-core' import {type CreateManifest, type CreateWorkspaceManifest} from './types.js' import {writeWorkspaceFiles} from './writeWorkspaceFiles.js' -const MANIFEST_FILENAME = 'create-manifest.json' +export const MANIFEST_FILENAME = 'create-manifest.json' const debug = subdebug('writeManifestFile') +export interface WriteManifestFileOptions { + /** + * Target directory for the manifest files. May be absolute or relative — relative + * paths are resolved against `workDir`. + */ + outPath: string + /** + * Studio root directory. Used to resolve a relative `outPath` and to look up the + * local `sanity` package version that gets stamped onto the manifest. + */ + workDir: string + /** Extracted workspace manifests to serialize alongside the top-level manifest. */ + workspaceManifests: CreateWorkspaceManifest[] +} + export async function writeManifestFile({ outPath, workDir, workspaceManifests, -}: { - outPath: string - workDir: string - workspaceManifests: CreateWorkspaceManifest[] -}) { +}: WriteManifestFileOptions) { const staticPath = isAbsolute(outPath) ? outPath : resolve(join(workDir, outPath)) debug('Writing manifest to %s', staticPath) const path = join(staticPath, MANIFEST_FILENAME) diff --git a/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts b/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts index 40d1c6b3d..7b858c1ce 100644 --- a/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/build.studio.test.ts @@ -79,6 +79,44 @@ describe('#build studio', {timeout: (platform() === 'win32' ? 120 : 60) * 1000}, const files = await readdir(outputFolder) expect(files).toContain('index.html') expect(files).toContain('static') + + // Federation artifacts should NOT be present when federation is not enabled + expect(files).not.toContain('federation') + expect(files).not.toContain('mf-manifest.json') + }) + + test('should build the "federated-studio" with only federation artifacts', async () => { + const cwd = await testFixture('federated-studio') + process.chdir(cwd) + + const {error, stderr} = await testCommand(BuildCommand, ['--yes'], { + config: {root: cwd}, + }) + + // 1. Build succeeds + if (error) throw error + expect(stderr).toContain('✔ Build Sanity Studio') + + const distFiles = await readdir(join(cwd, 'dist')) + + // 2. No client artifacts + expect(distFiles).not.toContain('index.html') + expect(distFiles).not.toContain('static') + expect(distFiles).not.toContain('vendor') + + // 3. Stable remote entry (unhashed) + expect(distFiles).toContain('remote-entry.js') + + // 4. Federation manifest (valid JSON) + expect(distFiles).toContain('mf-manifest.json') + const manifest = JSON.parse(await readFile(join(cwd, 'dist', 'mf-manifest.json'), 'utf8')) + expect(manifest).toHaveProperty('id') + expect(manifest).toHaveProperty('name') + + // 5. Hashed federation chunks + expect(distFiles).toContain('assets') + const assetFiles = await readdir(join(cwd, 'dist', 'assets')) + expect(assetFiles.some((f) => /^remote-entry-.+\.js$/.test(f))).toBe(true) }) test("should build the 'worst-case-studio' example", async () => { diff --git a/packages/@sanity/cli/src/commands/__tests__/deploy.app.test.ts b/packages/@sanity/cli/src/commands/__tests__/deploy.app.test.ts index 999dd038d..7fcedbad2 100644 --- a/packages/@sanity/cli/src/commands/__tests__/deploy.app.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/deploy.app.test.ts @@ -5,7 +5,7 @@ import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import {buildApp} from '../../actions/build/buildApp.js' import {checkDir} from '../../actions/deploy/checkDir.js' -import {extractAppManifest} from '../../actions/manifest/extractAppManifest.js' +import {extractCoreAppManifest} from '../../actions/manifest/extractCoreAppManifest.js' import {USER_APPLICATIONS_API_VERSION} from '../../services/userApplications.js' import {dirIsEmptyOrNonExistent} from '../../util/dirIsEmptyOrNonExistent.js' import {DeployCommand} from '../deploy.js' @@ -28,8 +28,8 @@ vi.mock('../../actions/deploy/checkDir.js', () => ({ checkDir: vi.fn(), })) -vi.mock('../../actions/manifest/extractAppManifest.js', () => ({ - extractAppManifest: vi.fn(), +vi.mock('../../actions/manifest/extractCoreAppManifest.js', () => ({ + extractCoreAppManifest: vi.fn(), })) vi.mock('@sanity/cli-core/ux', async () => { @@ -60,7 +60,7 @@ const mockInput = vi.mocked(input) const mockCheckDir = vi.mocked(checkDir) const mockDirIsEmptyOrNonExistent = vi.mocked(dirIsEmptyOrNonExistent) const mockBuildApp = vi.mocked(buildApp) -const mockExtractAppManifest = vi.mocked(extractAppManifest) +const mockExtractCoreAppManifest = vi.mocked(extractCoreAppManifest) const appId = 'app-id' const organizationId = 'org-id' @@ -86,7 +86,7 @@ describe('#deploy app', () => { }) mockCheckDir.mockResolvedValue() // Default to empty manifest for app deployments - mockExtractAppManifest.mockResolvedValue(undefined) + mockExtractCoreAppManifest.mockResolvedValue(undefined) }) afterEach(() => { @@ -212,7 +212,7 @@ describe('#deploy app', () => { const cwd = await testFixture('basic-app') process.cwd = () => cwd - mockExtractAppManifest.mockResolvedValue({ + mockExtractCoreAppManifest.mockResolvedValue({ title: 'New Title From Manifest', version: '1', }) @@ -276,7 +276,7 @@ describe('#deploy app', () => { process.cwd = () => cwd const sameTitle = 'Test App' - mockExtractAppManifest.mockResolvedValue({ + mockExtractCoreAppManifest.mockResolvedValue({ title: sameTitle, version: '1', }) @@ -1012,7 +1012,7 @@ describe('#deploy app', () => { version: '1' as const, } - mockExtractAppManifest.mockResolvedValue(manifest) + mockExtractCoreAppManifest.mockResolvedValue(manifest) mockApi({ apiVersion: USER_APPLICATIONS_API_VERSION, @@ -1066,7 +1066,7 @@ describe('#deploy app', () => { if (error) throw error expect(stdout).toContain('Success! Application deployed') - expect(mockExtractAppManifest).toHaveBeenCalled() + expect(mockExtractCoreAppManifest).toHaveBeenCalled() }) test('should test input validation for app title', async () => { diff --git a/packages/@sanity/cli/src/commands/__tests__/dev.test.ts b/packages/@sanity/cli/src/commands/__tests__/dev.test.ts index 8234c0583..76a13e3ca 100644 --- a/packages/@sanity/cli/src/commands/__tests__/dev.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/dev.test.ts @@ -3,7 +3,6 @@ import {createServer} from 'node:http' import {platform} from 'node:os' import {join} from 'node:path' -import {getProjectCliClient} from '@sanity/cli-core' import {confirm} from '@sanity/cli-core/ux' import {testCommand, testFixture} from '@sanity/cli-test' import {afterEach, describe, expect, test, vi} from 'vitest' @@ -25,14 +24,6 @@ vi.mock('../../util/compareDependencyVersions.js', () => ({ compareDependencyVersions: vi.fn().mockResolvedValue({mismatched: [], unresolvedPrerelease: []}), })) -const mockGetDashboardAppURL = vi.hoisted(() => - vi.fn().mockResolvedValue('https://www.sanity.io/@test-org?dev=http%3A%2F%2Flocalhost%3A5340'), -) - -vi.mock('../../actions/dev/getDashboardAppUrl.js', () => ({ - getDashboardAppURL: mockGetDashboardAppURL, -})) - vi.mock('@sanity/cli-core/ux', async () => { const actual = await vi.importActual('@sanity/cli-core/ux') return { @@ -44,11 +35,28 @@ vi.mock('@sanity/cli-core/ux', async () => { vi.mock('../../util/packageManager/upgradePackages.js') vi.mock('../../util/packageManager/packageManagerChoice.js') +// Prevent the workbench dev server from starting — it would shift ports (+1) +// and suppress output messages that tests assert on. +vi.mock('../../actions/dev/startWorkbenchDevServer.js', () => ({ + startWorkbenchDevServer: vi.fn().mockImplementation(async (options) => { + const {getSharedServerConfig} = await vi.importActual< + typeof import('../../util/getSharedServerConfig.js') + >('../../util/getSharedServerConfig.js') + + const {httpHost, httpPort} = getSharedServerConfig({ + cliConfig: options.cliConfig, + flags: {host: options.flags.host, port: options.flags.port}, + workDir: options.workDir, + }) + + return {close: async () => {}, httpHost, workbenchAvailable: false, workbenchPort: httpPort} + }), +})) + vi.mock('@sanity/cli-core', async () => { const actual = await vi.importActual('@sanity/cli-core') return { ...actual, - getProjectCliClient: vi.fn(), isInteractive: vi.fn(() => true), } }) @@ -58,7 +66,6 @@ const mockCompareDependencyVersions = vi.mocked(compareDependencyVersions) const mockConfirm = vi.mocked(confirm) const mockUpgradePackages = vi.mocked(upgradePackages) const mockGetPackageManagerChoice = vi.mocked(getPackageManagerChoice) -const mockGetProjectCliClient = vi.mocked(getProjectCliClient) describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { afterEach(() => { @@ -85,37 +92,16 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { }) if (error) throw error - expect(stdout).toContain('Dev server started on port 5333') - expect(stdout).toContain('View your app in the Sanity dashboard here:') + expect(stdout).toContain('App dev server started on port 5333') expect(stderr).toContain('Checking configuration files') await tryCloseServer(result) }) - test('should warn when --no-load-in-dashboard is used with app', async () => { - const cwd = await testFixture('basic-app') - process.cwd = () => cwd - - const {error, result, stderr, stdout} = await testCommand( - DevCommand, - ['--no-load-in-dashboard', '--port', '5334'], - { - config: {root: cwd}, - mocks: {isInteractive: true}, - }, - ) - - if (error) throw error - expect(stderr).toContain('Apps cannot run without the Sanity dashboard') - expect(stderr).toContain('Starting dev server with the --load-in-dashboard flag set to true') - expect(stdout).toContain('Dev server started on port 5334') - await tryCloseServer(result) - }) - - test('should automatically change port if conflicted', async () => { + test('should start on next available port when requested port is in use', async () => { const cwd = await testFixture('basic-app') process.cwd = () => cwd - // Create a server on port 5338 to block it + // Apps use strictPort: false, so Vite auto-selects the next available port const server = createServer() await new Promise((resolve) => { server.listen(5338, 'localhost', resolve) @@ -128,13 +114,10 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { }) if (error) throw error - // Should automatically pick a different port - expect(stdout).toMatch(/Dev server started on port \d{4}/) - expect(stdout).not.toContain('Dev server started on port 5338') - expect(stdout).toContain('View your app in the Sanity dashboard here:') + expect(stdout).toMatch(/App dev server started on port \d{4}/) + expect(stdout).not.toContain('App dev server started on port 5338') await tryCloseServer(result) } finally { - // Clean up the server await closeServer(server) } }) @@ -163,12 +146,6 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { vi.stubEnv('SANITY_APP_SERVER_HOSTNAME', '127.0.0.1') vi.stubEnv('SANITY_APP_SERVER_PORT', '5350') - mockGetDashboardAppURL.mockImplementationOnce(({httpHost, httpPort}) => - Promise.resolve( - `https://www.sanity.io/@test-org?dev=http%3A%2F%2F${httpHost}%3A${httpPort}`, - ), - ) - const cwd = await testFixture('basic-app') process.cwd = () => cwd @@ -178,13 +155,7 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { }) if (error) throw error - expect(stdout).toContain('Dev server started on port 5350') - expect(stdout).toContain('127.0.0.1') - expect(mockGetDashboardAppURL).toHaveBeenCalledWith({ - httpHost: '127.0.0.1', - httpPort: 5350, - organizationId: 'org-id', - }) + expect(stdout).toContain('App dev server started on port 5350') await tryCloseServer(result) }) @@ -238,7 +209,7 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { const {error, result, stdout} = await testCommand( DevCommand, - ['--host', '127.0.0.1', '--port', '5336'], + ['--host', '127.0.0.1', '--port', '5359'], { config: {root: cwd}, mocks: {isInteractive: true}, @@ -246,95 +217,10 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { ) if (error) throw error - expect(stdout).toContain('http://127.0.0.1:5336') + expect(stdout).toContain('http://127.0.0.1:5359') await tryCloseServer(result) }) - test('should start with load-in-dashboard', async () => { - const cwd = await testFixture('basic-studio') - process.cwd = () => cwd - - const projectId = 'test-project' - - // Need to modify the sanity config to include projectId for this test - const configPath = join(cwd, 'sanity.cli.ts') - const existingConfig = await readFile(configPath, 'utf8') - - // Add projectId to the config - const modifiedConfig = existingConfig.replace(/projectId:.*,/, `projectId: '${projectId}',`) - - await writeFile(configPath, modifiedConfig) - - mockGetProjectCliClient.mockResolvedValue({ - projects: { - getById: vi.fn().mockResolvedValue({organizationId: 'test-org'}), - }, - } as never) - - const {error, result, stderr, stdout} = await testCommand( - DevCommand, - ['--load-in-dashboard', '--port', '5340'], - { - config: {root: cwd}, - mocks: {isInteractive: true}, - }, - ) - - if (error) throw error - expect(stdout).toContain('Dev server started on port 5340') - expect(stdout).toContain('View your studio in the Sanity dashboard here:') - expect(stdout).toContain('https://www.sanity.io/@test-org?dev=http%3A%2F%2Flocalhost%3A5340') - expect(stderr).toContain('Checking configuration files') - - await tryCloseServer(result) - }) - - test('should error when projectId is missing with --load-in-dashboard', async () => { - const cwd = await testFixture('basic-studio') - process.cwd = () => cwd - - // Modify config to remove projectId - const configPath = join(cwd, 'sanity.cli.ts') - const existingConfig = await readFile(configPath, 'utf8') - const modifiedConfig = existingConfig.replace(/projectId:.*,/, '') - await writeFile(configPath, modifiedConfig) - - const {error} = await testCommand(DevCommand, ['--load-in-dashboard', '--port', '5343'], { - config: {root: cwd}, - mocks: {isInteractive: true}, - }) - - expect(error).toBeDefined() - expect(error?.message).toContain('Project Id is required to load in dashboard') - expect(error?.oclif?.exit).toBe(1) - }) - - test('should error when API fails to fetch organizationId', async () => { - const cwd = await testFixture('basic-studio') - process.cwd = () => cwd - - const projectId = 'test-project' - const configPath = join(cwd, 'sanity.cli.ts') - const existingConfig = await readFile(configPath, 'utf8') - const modifiedConfig = existingConfig.replace(/projectId:.*,/, `projectId: '${projectId}',`) - await writeFile(configPath, modifiedConfig) - - mockGetProjectCliClient.mockResolvedValue({ - projects: { - getById: vi.fn().mockRejectedValue(new Error('Project not found')), - }, - } as never) - - const {error} = await testCommand(DevCommand, ['--load-in-dashboard', '--port', '5344'], { - config: {root: cwd}, - mocks: {isInteractive: true}, - }) - - expect(error).toBeDefined() - expect(error?.message).toContain('Failed to get organization id from project id') - expect(error?.oclif?.exit).toBe(1) - }) - test('should start dev server successfully when user declines auto-updates', async () => { const cwd = await testFixture('basic-studio') process.cwd = () => cwd @@ -459,7 +345,7 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { test('should fallback to env variables when host and port flags not set', async () => { vi.stubEnv('SANITY_STUDIO_SERVER_HOSTNAME', '127.0.0.1') - vi.stubEnv('SANITY_STUDIO_SERVER_PORT', '5350') + vi.stubEnv('SANITY_STUDIO_SERVER_PORT', '5355') const cwd = await testFixture('basic-studio') process.cwd = () => cwd @@ -470,7 +356,7 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { }) if (error) throw error - expect(stdout).toContain('http://127.0.0.1:5350') + expect(stdout).toContain('http://127.0.0.1:5355') await tryCloseServer(result) }) @@ -484,7 +370,7 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { cliConfig: { server: { hostname: '127.0.0.1', - port: 5351, + port: 5357, }, }, isInteractive: true, @@ -492,35 +378,31 @@ describe('#dev', {timeout: (platform() === 'win32' ? 60 : 30) * 1000}, () => { }) if (error) throw error - expect(stdout).toContain('http://127.0.0.1:5351') + expect(stdout).toContain('http://127.0.0.1:5357') await tryCloseServer(result) }) }) - test('should throw an error if port is already in use', async () => { + test('should start on next available port when requested port is in use', async () => { const cwd = await testFixture('basic-studio') process.cwd = () => cwd - // Create a server on port 5337 to block it - const server = createServer() - await new Promise((resolve) => { - server.listen(5337, 'localhost', resolve) - }) + // Studios use strictPort: false, so Vite auto-selects the next available port + const server1 = createServer() + await new Promise((resolve) => server1.listen(5337, 'localhost', resolve)) try { - const {error, result} = await testCommand(DevCommand, ['--port', '5337'], { + const {error, result, stdout} = await testCommand(DevCommand, ['--port', '5337'], { config: {root: cwd}, mocks: {isInteractive: true}, }) + if (error) throw error + expect(stdout).toMatch(/running at http:\/\/localhost:\d{4}/) + expect(stdout).not.toContain('running at http://localhost:5337') await tryCloseServer(result) - - expect(error).toBeDefined() - expect(error?.message).toContain('Port 5337 is already in use') - expect(error?.oclif?.exit).toBe(1) } finally { - // Clean up the server - await closeServer(server) + await closeServer(server1) } }) }) diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts index f0e6c6803..0829ddcbc 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.authentication.test.ts @@ -170,6 +170,7 @@ describe('#init: authentication', () => { '--output-path=/test/output', '--no-overwrite-files', '--template=clean', + '--federation', ], { mocks: { @@ -214,6 +215,7 @@ describe('#init: authentication', () => { '--output-path=/test/output', '--no-overwrite-files', '--template=clean', + '--federation', ], { mocks: { diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts index 3f0a49e3f..76ab47c04 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.bootstrap-app.test.ts @@ -169,6 +169,7 @@ describe('#init: bootstrap-app-initialization', () => { '--dataset=test', '--package-manager=npm', '--typescript', + '--federation', ], { mocks: { @@ -182,6 +183,7 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: 'test', + federation: true, organizationId: undefined, output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), @@ -256,6 +258,7 @@ describe('#init: bootstrap-app-initialization', () => { '--output-path=/test/output', '--package-manager=npm', '--typescript', + '--federation', ], { mocks: { @@ -269,6 +272,7 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: '', + federation: true, organizationId: 'org-1', output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), @@ -331,6 +335,7 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: '', + federation: true, organizationId: 'org-1', output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), @@ -401,6 +406,7 @@ describe('#init: bootstrap-app-initialization', () => { autoUpdates: true, bearerToken: undefined, dataset: '', + federation: true, organizationId: 'org-1', output: expect.any(Object), outputPath: convertToSystemPath('/test/output'), diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts index 7aae59741..25c419fdc 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts @@ -513,6 +513,7 @@ describe('#init: create new project', () => { '--no-overwrite-files', '--template=moviedb', '--no-import-dataset', + '--federation', ], {mocks: {...defaultMocks, isInteractive: true}}, ) diff --git a/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts b/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts index 0b3c0752a..2d50818f9 100644 --- a/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts +++ b/packages/@sanity/cli/src/commands/__tests__/init/init.staging-env.test.ts @@ -223,6 +223,7 @@ describe('#init: staging env propagation', () => { '--dataset=test', '--package-manager=npm', '--typescript', + '--federation', ], { mocks: { @@ -260,6 +261,7 @@ describe('#init: staging env propagation', () => { '--dataset=test', '--package-manager=npm', '--typescript', + '--federation', ], { mocks: { @@ -289,6 +291,7 @@ describe('#init: staging env propagation', () => { '--dataset=test', '--package-manager=npm', '--typescript', + '--federation', ], { mocks: { diff --git a/packages/@sanity/cli/src/commands/dev.ts b/packages/@sanity/cli/src/commands/dev.ts index da136e49d..07b34af45 100644 --- a/packages/@sanity/cli/src/commands/dev.ts +++ b/packages/@sanity/cli/src/commands/dev.ts @@ -13,7 +13,6 @@ export class DevCommand extends SanityCommand { static override examples = [ '<%= config.bin %> <%= command.id %> --host=0.0.0.0', '<%= config.bin %> <%= command.id %> --port=1942', - '<%= config.bin %> <%= command.id %> --load-in-dashboard', ] static override flags = { @@ -24,10 +23,6 @@ export class DevCommand extends SanityCommand { host: Flags.string({ description: 'Local network interface to listen on (default: localhost)', }), - 'load-in-dashboard': Flags.boolean({ - allowNo: true, - description: 'Load the app/studio in the Sanity dashboard', - }), port: Flags.string({ description: 'TCP port to start server on (default: 3333)', }), @@ -40,14 +35,6 @@ export class DevCommand extends SanityCommand { const cliConfig = await this.getCliConfig() const isApp = determineIsApp(cliConfig) - // load-in-dashboard is defaulted to true for apps. - if (isApp && flags['load-in-dashboard'] === undefined) { - flags['load-in-dashboard'] = true - } else if (flags['load-in-dashboard'] === undefined) { - // For non-apps, load-in-dashboard is defaulted to false. - flags['load-in-dashboard'] = false - } - try { const result = await devAction({ cliConfig, diff --git a/packages/@sanity/cli/src/commands/init.ts b/packages/@sanity/cli/src/commands/init.ts index ca539ef9f..ebc7213f1 100644 --- a/packages/@sanity/cli/src/commands/init.ts +++ b/packages/@sanity/cli/src/commands/init.ts @@ -78,6 +78,11 @@ export class InitCommand extends SanityCommand { return input }, }), + federation: Flags.boolean({ + allowNo: true, + default: undefined, + description: 'Enable federation for this project', + }), 'from-create': Flags.boolean({ description: 'Internal flag to indicate that the command is run from create-sanity', hidden: true, diff --git a/packages/@sanity/cli/src/commands/manifest/extract.ts b/packages/@sanity/cli/src/commands/manifest/extract.ts index e98961d42..a27e3eeec 100644 --- a/packages/@sanity/cli/src/commands/manifest/extract.ts +++ b/packages/@sanity/cli/src/commands/manifest/extract.ts @@ -1,5 +1,5 @@ import {Flags} from '@oclif/core' -import {SanityCommand} from '@sanity/cli-core' +import {findProjectRoot, SanityCommand} from '@sanity/cli-core' import {manifestDebug} from '../../actions/manifest/debug.js' import {extractManifest} from '../../actions/manifest/extractManifest.js' @@ -37,7 +37,12 @@ export class ExtractManifestCommand extends SanityCommand { + return confirm({ + default: true, + message: 'Would you like to enable federation for this project?', + }) +} diff --git a/packages/@sanity/cli/src/server/devServer.ts b/packages/@sanity/cli/src/server/devServer.ts index 773013edb..4e453dfe7 100644 --- a/packages/@sanity/cli/src/server/devServer.ts +++ b/packages/@sanity/cli/src/server/devServer.ts @@ -21,9 +21,11 @@ export interface DevServerOptions { appTitle?: string entry?: string + federation?: CliConfig['federation'] httpHost?: string isApp?: boolean projectName?: string + reactRefreshHost?: string schemaExtraction?: CliConfig['schemaExtraction'] typegen?: CliConfig['typegen'] vite?: UserViteConfig @@ -42,10 +44,12 @@ export async function startDevServer(options: DevServerOptions): Promise vi.fn()) + +vi.mock('get-latest-version', () => ({ + getLatestVersion: mockGetLatestVersion, +})) + +describe('resolveLatestVersions', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + test('passes through valid semver ranges without looking them up', async () => { + const result = await resolveLatestVersions({ + foo: '1.2.3', + react: '^19.2.4', + typescript: '~5.8', + }) + + expect(result).toEqual({ + foo: '1.2.3', + react: '^19.2.4', + typescript: '~5.8', + }) + expect(mockGetLatestVersion).not.toHaveBeenCalled() + }) + + test('resolves the `latest` dist-tag and caret-prefixes the version', async () => { + mockGetLatestVersion.mockResolvedValueOnce('4.5.6') + + const result = await resolveLatestVersions({sanity: 'latest'}) + + expect(mockGetLatestVersion).toHaveBeenCalledWith('sanity', {range: 'latest'}) + expect(result).toEqual({sanity: '^4.5.6'}) + }) + + test('passes arbitrary dist-tags such as `workbench` through without resolving', async () => { + const result = await resolveLatestVersions({sanity: 'workbench'}) + + expect(mockGetLatestVersion).not.toHaveBeenCalled() + expect(result).toEqual({sanity: 'workbench'}) + }) + + test('resolves `latest` alongside pass-through ranges and dist-tags in a single call', async () => { + mockGetLatestVersion.mockResolvedValueOnce('1.0.0') + + const result = await resolveLatestVersions({ + '@sanity/vision': 'latest', + react: '^19.2.4', + sanity: 'workbench', + }) + + expect(result).toEqual({ + '@sanity/vision': '^1.0.0', + react: '^19.2.4', + sanity: 'workbench', + }) + expect(mockGetLatestVersion).toHaveBeenCalledTimes(1) + expect(mockGetLatestVersion).toHaveBeenCalledWith('@sanity/vision', {range: 'latest'}) + }) +}) diff --git a/packages/@sanity/cli/src/util/resolveReactStrictMode.ts b/packages/@sanity/cli/src/util/resolveReactStrictMode.ts new file mode 100644 index 000000000..e7a38cd9c --- /dev/null +++ b/packages/@sanity/cli/src/util/resolveReactStrictMode.ts @@ -0,0 +1,8 @@ +import {type CliConfig} from '@sanity/cli-core' + +export function resolveReactStrictMode(cliConfig?: CliConfig): boolean { + if (process.env.SANITY_STUDIO_REACT_STRICT_MODE) { + return process.env.SANITY_STUDIO_REACT_STRICT_MODE === 'true' + } + return Boolean(cliConfig?.reactStrictMode) +} diff --git a/packages/@sanity/cli/templates/shared/gitignore.txt b/packages/@sanity/cli/templates/shared/gitignore.txt index aa9909c01..de5933301 100644 --- a/packages/@sanity/cli/templates/shared/gitignore.txt +++ b/packages/@sanity/cli/templates/shared/gitignore.txt @@ -27,3 +27,6 @@ # Dotenv and similar local-only files *.local + +# Module federation temporary files +.__mf__temp/ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 143a73639..e8872977d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,13 +188,13 @@ importers: version: 6.1.3 turbo: specifier: ^2.9.6 - version: 2.9.14 + version: 2.9.6 typescript: specifier: 'catalog:' version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) fixtures/basic-app: dependencies: @@ -212,7 +212,7 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) devDependencies: '@types/react': specifier: ^19.2.14 @@ -232,13 +232,13 @@ importers: devDependencies: sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) fixtures/basic-studio: dependencies: '@sanity/vision': specifier: 'catalog:' - version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: specifier: ^19.2.5 version: 19.2.5 @@ -247,7 +247,29 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + styled-components: + specifier: ^6.4.0 + version: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + devDependencies: + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + fixtures/federated-studio: + dependencies: + react: + specifier: ^19.2.5 + version: 19.2.5 + react-dom: + specifier: ^19.2.5 + version: 19.2.5(react@19.2.5) + sanity: + specifier: 'catalog:' + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: specifier: ^6.4.0 version: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -263,7 +285,7 @@ importers: dependencies: '@sanity/vision': specifier: 'catalog:' - version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: specifier: ^19.2.5 version: 19.2.5 @@ -272,7 +294,7 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: specifier: ^6.4.0 version: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -288,7 +310,7 @@ importers: dependencies: '@sanity/vision': specifier: 'catalog:' - version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: specifier: ^19.2.5 version: 19.2.5 @@ -297,7 +319,7 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: specifier: ^6.4.0 version: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -344,7 +366,7 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) devDependencies: '@types/react': specifier: ^19.2.14 @@ -363,7 +385,7 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: specifier: ^6.4.0 version: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -379,10 +401,10 @@ importers: dependencies: '@sanity/code-input': specifier: ^7.1.0 - version: 7.1.0(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 7.1.0(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@sanity/vision': specifier: 'catalog:' - version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: specifier: ^19.2.5 version: 19.2.5 @@ -391,10 +413,10 @@ importers: version: 19.2.5(react@19.2.5) sanity: specifier: 'catalog:' - version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + version: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) sanity-plugin-media: specifier: ^4.1.1 - version: 4.1.1(@emotion/is-prop-valid@1.4.0)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 4.1.1(@emotion/is-prop-valid@1.4.0)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) styled-components: specifier: ^6.4.0 version: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -435,7 +457,7 @@ importers: version: 10.2.1(jiti@2.7.0) vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) packages/@repo/package.config: devDependencies: @@ -508,6 +530,9 @@ importers: '@sanity/export': specifier: ^6.1.0 version: 6.1.0 + '@sanity/federation': + specifier: 0.1.0-alpha.7 + version: 0.1.0-alpha.7(debug@4.4.3)(rollup@4.60.2)(typescript@5.9.3)(vite@7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) '@sanity/generate-help-url': specifier: ^4.0.0 version: 4.0.0 @@ -522,7 +547,7 @@ importers: version: 6.1.2(@oclif/core@4.10.6)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(xstate@5.30.0) '@sanity/runtime-cli': specifier: ^15.0.2 - version: 15.0.2(@types/node@20.19.39)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4) + version: 15.1.1(@types/node@20.19.39)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4) '@sanity/schema': specifier: 'catalog:' version: 5.23.0(@types/react@19.2.14) @@ -845,7 +870,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) packages/@sanity/cli-core: dependencies: @@ -1005,7 +1030,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) packages/@sanity/cli-test: dependencies: @@ -1075,7 +1100,7 @@ importers: version: 5.9.3 vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) yaml: specifier: 'catalog:' version: 2.8.4 @@ -1131,7 +1156,7 @@ importers: version: 0.3.18 vitest: specifier: 'catalog:' - version: 4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + version: 4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) packages: @@ -2036,9 +2061,6 @@ packages: '@codemirror/autocomplete@6.20.1': resolution: {integrity: sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==} - '@codemirror/autocomplete@6.20.2': - resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} - '@codemirror/commands@6.10.3': resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} @@ -2078,9 +2100,6 @@ packages: '@codemirror/search@6.6.0': resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==} - '@codemirror/search@6.7.0': - resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} - '@codemirror/state@6.6.0': resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} @@ -2090,9 +2109,6 @@ packages: '@codemirror/view@6.40.0': resolution: {integrity: sha512-WA0zdU7xfF10+5I3HhUUq3kqOx3KjqmtQ9lqZjfK7jtYk4G72YW9rezcSywpaUMCWOMlq+6E0pO1IWg1TNIhtg==} - '@codemirror/view@6.43.0': - resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} - '@commitlint/cli@20.4.2': resolution: {integrity: sha512-YjYSX2yj/WsVoxh9mNiymfFS2ADbg2EK4+1WAsMuckwKMCqJ5PDG0CJU/8GvmHWcv4VRB2V02KqSiecRksWqZQ==} engines: {node: '>=v18'} @@ -2277,6 +2293,9 @@ packages: '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.2': resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} @@ -2918,8 +2937,8 @@ packages: '@types/node': optional: true - '@inquirer/checkbox@5.1.4': - resolution: {integrity: sha512-w6KF8ZYRvqHhROkOTHXYC3qIV/KYEu5o12oLqQySvch61vrYtRxNSHTONSdJqWiFJPlCUQAHT5OgOIyuTr+MHQ==} + '@inquirer/checkbox@5.1.5': + resolution: {integrity: sha512-Jmf9tgBHIEK5SAOB7swYfStqmtkZb00xOTpSQmkoGEpdxOTpJi9RS0A8bkfDPHTTItZRJrRdZrEMu25wyj0VfQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -2949,8 +2968,8 @@ packages: '@types/node': optional: true - '@inquirer/confirm@6.0.12': - resolution: {integrity: sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==} + '@inquirer/confirm@6.0.13': + resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -2967,8 +2986,8 @@ packages: '@types/node': optional: true - '@inquirer/core@11.1.7': - resolution: {integrity: sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==} + '@inquirer/core@11.1.10': + resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -2976,8 +2995,8 @@ packages: '@types/node': optional: true - '@inquirer/core@11.1.9': - resolution: {integrity: sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==} + '@inquirer/core@11.1.7': + resolution: {integrity: sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3007,8 +3026,8 @@ packages: '@types/node': optional: true - '@inquirer/editor@5.1.1': - resolution: {integrity: sha512-6y11LgmNpmn5D2aB5FgnCfBUBK8ZstwLCalyJmORcJZ/WrhOjm16mu6eSqIx8DnErxDqSLr+Jkp+GP8/Nwd5tA==} + '@inquirer/editor@5.1.2': + resolution: {integrity: sha512-Y3Nor7S/DhIPo+8Ym/dSY4efwKI4BsflKDwXh0jNeXJsSF3dteS/3Yf+z4wkibVZDvYMyCgknSTQlNahfunGHg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3034,8 +3053,8 @@ packages: '@types/node': optional: true - '@inquirer/expand@5.0.13': - resolution: {integrity: sha512-dF2zvrFo9LshkcB23/O1il13kBkBltWIXzut1evfbuBLXMiGIuC45c+ZQ0uukjCDsvI8OWqun4FRYMnzFCQa3g==} + '@inquirer/expand@5.0.14': + resolution: {integrity: sha512-qyY9zcIX2eKYwaAUiQo9zORd61Lc3sXeM72fVbeHkYnDkqfr8/armcRbmVAIrExeJhI2puk+uomeKtWrpUVUmQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3104,8 +3123,8 @@ packages: '@types/node': optional: true - '@inquirer/input@5.0.12': - resolution: {integrity: sha512-uiMFBl4LqFzJClh80Q3f9hbOFJ6kgkDWI4LjAeBuyO6EanVVMF69AgOvpi1qdqjDSjDN6578B6nky9ceEpI+1Q==} + '@inquirer/input@5.0.13': + resolution: {integrity: sha512-0l0jCHlJnXIV8CTxwQC0C+5Ziq8WP22edWgmciW2xYvoeoSck4v5FvCS1ctKdqLLR0dUo93uAHgWHywgBSoRyw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3131,8 +3150,8 @@ packages: '@types/node': optional: true - '@inquirer/number@4.0.12': - resolution: {integrity: sha512-/vrwhEf7Xsuh+YlHF4IjSy3g1cyrQuPaSiHIxCEbLu8qnfvrcvJyCkoktOOF+xV9gSb77/G0n3h04RbMDW2sIg==} + '@inquirer/number@4.0.13': + resolution: {integrity: sha512-WHmkYnnJAou5gx7RgcvAfUggnHNM1zWfoh0dFPl3dxVssuqt+dK5rIbaOYQXNyOegvFnopbKupjnhw2O8gANNg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3158,8 +3177,8 @@ packages: '@types/node': optional: true - '@inquirer/password@5.0.12': - resolution: {integrity: sha512-CBh7YHju623lxJRcAOo498ZUwIuMy63bqW/vVq0tQAZVv+lkWlHkP9ealYE1utWSisEShY5VMdzIXRmyEODzcQ==} + '@inquirer/password@5.0.13': + resolution: {integrity: sha512-XDGu64ROHZjOOXLAANvJN7iIxWKhOSCG5VakrZ5kaScVR+snVJCFglD/hL3/677awtWcu4pXoWa280CDIYcBeg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3185,8 +3204,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@8.4.2': - resolution: {integrity: sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==} + '@inquirer/prompts@8.4.3': + resolution: {integrity: sha512-ai5LseTw9HhegupIgmo4cn7RpnCGznjjXu4OI+7jMR8vu7T1ZCCNMzFFAovUCjL1fl0cceksIN1++yQE59SmZw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3212,8 +3231,8 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@5.2.8': - resolution: {integrity: sha512-Su7FQvp5buZmCymN3PPoYv31ZQQX4ve2j02k7piGgKAWgE+AQRB5YoYVveGXcl3TZ9ldgRMSxj56YfDFmmaqLg==} + '@inquirer/rawlist@5.2.9': + resolution: {integrity: sha512-a1ErXEfgjfPYpyQ89dp+7n2IISjH9oQg3ygvF5adz8B7aHn4n2PjEgu1wpVTp69K3bj3lVLxP0qJ2b1clk1Whw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3239,8 +3258,8 @@ packages: '@types/node': optional: true - '@inquirer/search@4.1.8': - resolution: {integrity: sha512-fGiHKGD6DyPIYUWxoXnQTeXeyYqSOUrasDMABBmMHUalH/LxkuzY0xVRtimXAt1sUeeyYkVuKQx1bebMuN11Kw==} + '@inquirer/search@4.1.9': + resolution: {integrity: sha512-ZlbM28Q9lmLkFPNAIv+ZuY530n5Km8U1WW48oYEvDhe9yc2uL3m3t+JSdRUkQlk5fuIuskgiIVjcb7czFzQpuA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3270,8 +3289,8 @@ packages: '@types/node': optional: true - '@inquirer/select@5.1.4': - resolution: {integrity: sha512-2kWcGKPMLAXAWRp1AH1SLsQmX+j0QjeljyXMUji9WMZC8nRDO0b7qquIGr6143E7KMLt3VAIGNXzwa/6PXQs4Q==} + '@inquirer/select@5.1.5': + resolution: {integrity: sha512-6SRg6kHfK/sjLXOsuqNebuir+sjwrf/iWuRUnXgB2slzEewppI1WfzeS16XxDcOQmXBruMmmB9Cgrz7wsAxqMg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} peerDependencies: '@types/node': '>=18' @@ -3354,9 +3373,6 @@ packages: '@lezer/common@1.5.1': resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} - '@lezer/common@1.5.2': - resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - '@lezer/css@1.3.0': resolution: {integrity: sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==} @@ -3415,6 +3431,60 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@module-federation/dts-plugin@2.3.1': + resolution: {integrity: sha512-6BJvu+dLDtW/ngpyuOLgpKOgtOnMUTZY51JUyargVckerKRbe7Ul+414YaHj32mu2FpsiHVMl4ig1XnxgnRg2Q==} + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: '>=1.0.24' + peerDependenciesMeta: + vue-tsc: + optional: true + + '@module-federation/error-codes@2.3.1': + resolution: {integrity: sha512-s3IjT2OYrSBNNmxdTmmrWBpsFfeNszdL6BSqjXLHb1CgXWUYLNXpb05IopnzMhRLcur6MTGuKR0ZSjJbmvQBbg==} + + '@module-federation/error-codes@2.3.3': + resolution: {integrity: sha512-UVtKBoKnRDcHgByIDvPRZSxQqjqbNH7NvJm1KHLoce33+EDiIdZYs0HvvUQv43RgESpB9s7HjrqFlq3bEcAgfQ==} + + '@module-federation/managers@2.3.1': + resolution: {integrity: sha512-kK/4FkoaIxbJbN+R6+cq+igv095hPox8oheZOKkrYA9P6Xv5FiHza+gHlCntiWTMrU8bzqJHH4VYm6gq1RB+dQ==} + + '@module-federation/runtime-core@2.3.1': + resolution: {integrity: sha512-E0WgaCn32AWzD0n6SCH7VQ+kxk46XyX432PQWARgyQzCX/wyLkaT+We3A18RVNUevRT85YHLrrVIhMKJJVHgjA==} + + '@module-federation/runtime-core@2.3.3': + resolution: {integrity: sha512-B07LDH9KxhBO3GbULGW64mQFVQBtrEd3PoaCBm7XR1IbU8rMQUJQjDNVZgXYcyhRPBVP+3KWZuiaKFRiNb6PQw==} + + '@module-federation/runtime@2.3.1': + resolution: {integrity: sha512-NiKelHKzOf1Vz8oqcxC/XRUAW224O6lKj9xD0cfp5Bp343iu6s58RlLvX1ypF+UpCl3jA4JM8npGax/3jjyifw==} + + '@module-federation/runtime@2.3.3': + resolution: {integrity: sha512-JYJ3qv9V85DtBtT/ppDuJNwBTUrYqqZDYcyiTzwY5+44dC5QPvgJ//F+BOhAhZ02WkZV0b4jsKTyLOC3vXKGqQ==} + + '@module-federation/sdk@2.3.1': + resolution: {integrity: sha512-lgWxFZyLRKDXWRGlV6ROjFJ6MRaJTxs0bBnS6hS9ONfr/0TkeW4JzDbsfzrB8g4p6IgSKB+wQ9XfibJCGBI5OQ==} + peerDependencies: + node-fetch: ^3.3.2 + peerDependenciesMeta: + node-fetch: + optional: true + + '@module-federation/sdk@2.3.3': + resolution: {integrity: sha512-mwCS+LQdqiSc6fM5iz/S60ibaFNSH6kNqlZkCRIuS4yjdZ+jgnihz+6xp1QzppvfFgKLhEHBiXOmcYOdk3Ckew==} + peerDependencies: + node-fetch: ^3.3.2 + peerDependenciesMeta: + node-fetch: + optional: true + + '@module-federation/third-party-dts-extractor@2.3.1': + resolution: {integrity: sha512-YpTLzM7H9damh31JX7eFBiCCR1mbibzS4i4JEa4fZ5ICT4hfNIuaAx1OeICGDOzSdl35TYegegCjk91oX6xCJQ==} + + '@module-federation/vite@1.14.0': + resolution: {integrity: sha512-RBUbUrCrpsjteNYAw21tKRLvTtAtKYkJz4GZKF2R5vEuVR1zQ42l4PC3c9b6ERj9wVfkKDHNWUdXfR5NAZ92Gw==} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + '@mswjs/interceptors@0.41.2': resolution: {integrity: sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==} engines: {node: '>=18'} @@ -3644,8 +3714,8 @@ packages: resolution: {integrity: sha512-ySCOYnPKZE3KACT1V9It99hWG9b8E5MpagbRdWxPNRO3beMqmbr4SLUQoFtZ9XRtW++kks1ZVwZOdpnR8rpb9A==} engines: {node: '>=18.0.0'} - '@oclif/core@4.11.0': - resolution: {integrity: sha512-nTkRMgxFlIKQIIYGvhO2JMsLSQ1aHPHblHfFgxgoBrGK8Ao/8wxc4eNOIv/+t8dMXliZd7mREVr6la4aXXXg5A==} + '@oclif/core@4.11.3': + resolution: {integrity: sha512-gQCSYAtUhJilGKaSaZhqejH9X1dDu+jWQjLmtGOgN/XcKaAEPPSeT2mu1UvlvtPox1/NNRdlBcUa8KRKo2HnJQ==} engines: {node: '>=18.0.0'} '@oclif/core@4.9.0': @@ -3656,6 +3726,10 @@ packages: resolution: {integrity: sha512-avWOKYmjANtyu8ipju/kopIIrSrbS/scJjiZTpBp/HKEHNm46v5riOo5LQj6MZ4bYJVQEoyHPg/2Seig5Ilkjw==} engines: {node: '>=18.0.0'} + '@oclif/plugin-help@6.2.49': + resolution: {integrity: sha512-fEsO0YU7ThtzHE1RGuoHxFu/OGlqxm7PCfFp+U1PS8sde4E0cDqjVDuv78+VKrr45LpC5lWOApj7pm3FNfHrVA==} + engines: {node: '>=18.0.0'} + '@oclif/plugin-not-found@3.2.81': resolution: {integrity: sha512-M88tLONBH36hLAbkFbmCo1hoZPSdU5l8Px1xEIlIgSmGMam+CoAzx4kGqpLbokgfpaHeP8/Jx3QJ18u9ef/2Qw==} engines: {node: '>=18.0.0'} @@ -4393,139 +4467,277 @@ packages: rollup: optional: true + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm-eabi@4.60.2': resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] os: [android] + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + '@rollup/rollup-android-arm64@4.60.2': resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} cpu: [arm64] os: [android] + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-arm64@4.60.2': resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.60.2': resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} cpu: [x64] os: [darwin] + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.60.2': resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.60.2': resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} cpu: [x64] os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] libc: [glibc] + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-arm-musleabihf@4.60.2': resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] libc: [musl] + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-arm64-gnu@4.60.2': resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-arm64-musl@4.60.2': resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] libc: [musl] + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-loong64-gnu@4.60.2': resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-loong64-musl@4.60.2': resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] libc: [musl] + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-ppc64-gnu@4.60.2': resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-ppc64-musl@4.60.2': resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] libc: [musl] + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-riscv64-gnu@4.60.2': resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-riscv64-musl@4.60.2': resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] libc: [musl] + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-s390x-gnu@4.60.2': resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] libc: [glibc] + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rollup/rollup-linux-x64-gnu@4.60.2': resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] libc: [glibc] + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + libc: [musl] + '@rollup/rollup-linux-x64-musl@4.60.2': resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] libc: [musl] + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + '@rollup/rollup-openbsd-x64@4.60.2': resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} cpu: [x64] os: [openbsd] + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.60.2': resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} cpu: [arm64] os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.60.2': resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.60.2': resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-gnu@4.60.2': resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.60.2': resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} cpu: [x64] @@ -4561,6 +4773,10 @@ packages: '@rushstack/ts-command-line@5.3.9': resolution: {integrity: sha512-GIHqU+sRGQ3LGWAZu1O+9Yh++qwtyNIIGuNbcWHJjBTm2qRez0cwINUHZ+pQLR8UuzZDcMajrDaNbUYoaL/XtQ==} + '@sanity-labs/design-tokens@0.0.2-alpha.2': + resolution: {integrity: sha512-rM0+qso6XaE60UaJ7z7v7DWBRlJ57XQUzuL8nTwtub42xGw4jusEhDq7/c4Mrdec98hLykyhe6t5n2mKxf/BVQ==} + engines: {node: '>=20.19 <22 || >=22.12'} + '@sanity/asset-utils@2.3.0': resolution: {integrity: sha512-dlEmALjQ5iyQG0O8ZVmkkE3wUYCKfRmiyMvuuGN5SF9buAHxmseBOKJ/Iy2DU/8ef70mtUXlzeCRSlTN/nmZsg==} engines: {node: '>=18'} @@ -4580,8 +4796,8 @@ packages: resolution: {integrity: sha512-6FdEcZMBuxdtfpCikb4l4yqnluxoGj4S75WDNtAXbNVjXJicpFzjwjMeoGtmsKcBbj80Lll1UONydqnYQRbILw==} engines: {node: '>=20'} - '@sanity/blueprints@0.17.1': - resolution: {integrity: sha512-gJyoz0YKwph5vpYJPqgd+Eq4GdGGfqKwDsoYpOdPcBJHq/0fU80eUXT6XlAsufpSYTmbQG93ARa1SbWqqYutyQ==} + '@sanity/blueprints@0.18.0': + resolution: {integrity: sha512-iEFEDtfBt12PiMbqmI4khLVvAhsYDX7OCLnfQ8OvgbhZzrzjsDDzAIrAOoXNmAfW/CgkUMG9qdYqhu1aTAI0dg==} engines: {node: '>=20'} '@sanity/browserslist-config@1.0.5': @@ -4651,6 +4867,12 @@ packages: engines: {node: '>=20.19 <22 || >=22.12'} hasBin: true + '@sanity/federation@0.1.0-alpha.7': + resolution: {integrity: sha512-4N7BkgxyjmdTMX3dLGq3F+XJFN7O9qY75m/Ns487eeQrLWyKBtCl+3Ct37a9FeEVdAiDvfSv2TzanbySo1vsyA==} + engines: {node: '>=20.19.1 <22 || >=22.12'} + peerDependencies: + vite: ^7.0.0 || ^8.0.0 + '@sanity/functions@1.3.1': resolution: {integrity: sha512-k/DOh7PTPFYrE9ryAagWETpgt0yhqc4gN5Eti3k1nwndFEpveg9z+I1s4NI7viWt8QCYpWpeW9ixSDZeTdczdA==} engines: {node: '>=20.19'} @@ -4735,6 +4957,9 @@ packages: xstate: optional: true + '@sanity/mutator@5.20.0': + resolution: {integrity: sha512-xqrNrP8w4yKKUP3EQAjKCH0bSRJvb6ssrB6wBSYwh9vDnW7OjZ2KJZSRn2cH7tL0SjPdKygbf9nCW4s85HjPCw==} + '@sanity/mutator@5.23.0': resolution: {integrity: sha512-riilTMit5XLg8C8GozQz9EsPrHuaK0Klsfczjv0dZ3p2srO/q/vJeM1uD4EB1JF7+jO8LV0lVSikMvbqBNmhWw==} @@ -4771,8 +4996,8 @@ packages: prismjs: optional: true - '@sanity/runtime-cli@15.0.2': - resolution: {integrity: sha512-+4zKz6EXBmdMqStf08SX2mWaYRz/aWD6DVMdJ/mibI2VOV9VT+5TsgVQHeKcn/kIXgaVwEO3Yo+0fbnKdo6lMA==} + '@sanity/runtime-cli@15.1.1': + resolution: {integrity: sha512-eraFR2KWkPI+bIg7ocfWdzKiQ3jD0Pfs+7UPsTrkZHuUl9GjQ3N6FfIfLEUSgthlfrurCZMMndFMsqhmYUBgxQ==} engines: {node: '>=20.19'} hasBin: true @@ -4813,6 +5038,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + '@sanity/types@5.20.0': + resolution: {integrity: sha512-mcCX5neb8BTOLAqlHc/1Q3fECcEvXv7pqqBCh3ZrgUEL/Uj4B8LiOXIPtp8X27sCZw/hqB1a15wOTExoxqerSQ==} + peerDependencies: + '@types/react': ^19.2 + '@sanity/types@5.23.0': resolution: {integrity: sha512-3OI74/OdtQlmQklYYPQo7IAUbjxitAaV0EZNIWOGt296so2vucSof4JrIfFRDGRT3+FmmdxCVazJTOrKBQfYhA==} peerDependencies: @@ -5266,33 +5496,33 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@turbo/darwin-64@2.9.14': - resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} + '@turbo/darwin-64@2.9.6': + resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.14': - resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} + '@turbo/darwin-arm64@2.9.6': + resolution: {integrity: sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.14': - resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} + '@turbo/linux-64@2.9.6': + resolution: {integrity: sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.14': - resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} + '@turbo/linux-arm64@2.9.6': + resolution: {integrity: sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.14': - resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} + '@turbo/windows-64@2.9.6': + resolution: {integrity: sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.14': - resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} + '@turbo/windows-arm64@2.9.6': + resolution: {integrity: sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A==} cpu: [arm64] os: [win32] @@ -5498,6 +5728,12 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.57.0': + resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/tsconfig-utils@8.59.0': resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5515,6 +5751,10 @@ packages: resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.57.0': + resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.59.0': resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5817,6 +6057,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + adm-zip@0.5.17: resolution: {integrity: sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==} engines: {node: '>=12.0'} @@ -5933,6 +6177,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + attr-accept@2.2.5: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} @@ -5940,6 +6188,9 @@ packages: aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + axios@1.13.5: + resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + b4a@1.7.3: resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==} peerDependencies: @@ -6072,6 +6323,11 @@ packages: browserify-zlib@0.1.4: resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} + browserslist@4.28.1: + resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -6144,8 +6400,11 @@ packages: camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} - caniuse-lite@1.0.30001790: - resolution: {integrity: sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==} + caniuse-lite@1.0.30001766: + resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -6380,6 +6639,10 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -6527,6 +6790,9 @@ packages: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6613,8 +6879,11 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.344: - resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} + electron-to-chromium@1.5.278: + resolution: {integrity: sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw==} + + electron-to-chromium@1.5.340: + resolution: {integrity: sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -6883,6 +7152,10 @@ packages: exif-component@1.0.1: resolution: {integrity: sha512-FXnmK9yJYTa3V3G7DE9BRjUJ0pwXMICAxfbsAuKPTuSlFzMZhQbcvvwx0I8ofNJHxz3tfjze+whxcGpfklAWOQ==} + expand-tilde@2.0.2: + resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} + engines: {node: '>=0.10.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -7001,6 +7274,14 @@ packages: resolution: {integrity: sha512-Z+suHH+7LSE40WfUeZPIxSxypCWvrzdVc60xAjUShZeT5eMWM0/FQUduq3HjluyfAHWvC/aOBkT1pTZktyF/jg==} engines: {node: '>= 0.12'} + find-file-up@2.0.1: + resolution: {integrity: sha512-qVdaUhYO39zmh28/JLQM5CoYN9byEOKEH4qfa8K1eNV17W0UUMJ9WgbR/hHFH+t5rcl+6RTb5UC7ck/I+uRkpQ==} + engines: {node: '>=8'} + + find-pkg@2.0.0: + resolution: {integrity: sha512-WgZ+nKbELDa6N3i/9nrHeNznm+lY3z4YfhDDWgW+5P0pdmMj26bxaxU11ookgY3NyP9GC7HvZ9etp0jRFqGEeQ==} + engines: {node: '>=8'} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -7038,6 +7319,15 @@ packages: resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==} engines: {node: '>=10'} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + form-data-encoder@2.1.4: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} @@ -7081,6 +7371,10 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -7141,6 +7435,9 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -7178,6 +7475,14 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} + global-modules@1.0.0: + resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==} + engines: {node: '>=0.10.0'} + + global-prefix@1.0.2: + resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==} + engines: {node: '>=0.10.0'} + globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} @@ -7215,6 +7520,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + groq-js@1.29.0: + resolution: {integrity: sha512-LP/O1GwdCpKk4X/+GtUNafOLvPMf8oU+kLbe6QdqUQQl/lOOirHcpS/Br6HRrb0VeVl9QKJzmS/dK7lHO1LYsg==} + engines: {node: '>= 14'} + groq-js@1.30.1: resolution: {integrity: sha512-l9U2cAN2CHF8o9+ApOWuyntnmaRa2mzSWnnDjDuJlaB48Yjc9FA16/gPEIZ5V9k4ug0l1EZYRiWeSztngeP5sQ==} engines: {node: '>= 14'} @@ -7254,10 +7563,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -7280,6 +7585,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + homedir-polyfill@1.0.3: + resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} + engines: {node: '>=0.10.0'} + hosted-git-info@7.0.2: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} @@ -7442,10 +7751,6 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -7609,6 +7914,11 @@ packages: resolution: {integrity: sha512-a9+LQqylQCU8f1zmsYmg2tfrbdY2YS/Hc+xntcq/mDI2MY3Q108nq8K23BWDIg6YGC5JsUMC15fj2ZMqCzt/+A==} engines: {node: '>=20.19.5'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -7629,6 +7939,10 @@ packages: javascript-stringify@2.1.0: resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true @@ -7909,6 +8223,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long-timeout@0.1.1: + resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -7934,6 +8251,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -8188,8 +8509,15 @@ packages: node-pty@1.1.0: resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + node-schedule@2.1.1: + resolution: {integrity: sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==} + engines: {node: '>=6'} normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} @@ -8405,6 +8733,10 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parse-passwd@1.0.0: + resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==} + engines: {node: '>=0.10.0'} + parse-path@7.1.0: resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} @@ -8415,6 +8747,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parse5@8.0.1: resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} @@ -8524,10 +8859,6 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.13: - resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -8585,6 +8916,9 @@ packages: protocols@2.0.2: resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + publint@0.3.18: resolution: {integrity: sha512-JRJFeBTrfx4qLwEuGFPk+haJOJN97KnPuK01yj+4k/Wj5BgoOK5uNsivporiqBjk2JDaslg7qJOhGRnpltGeog==} engines: {node: '>=18'} @@ -8848,6 +9182,10 @@ packages: resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-dir@1.0.1: + resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -8864,9 +9202,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true responselike@3.0.0: @@ -8928,6 +9265,11 @@ packages: esbuild: '>=0.18.0' rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rollup@4.60.2: resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9027,6 +9369,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -9110,6 +9457,9 @@ packages: resolution: {integrity: sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA==} hasBin: true + sorted-array-functions@1.3.0: + resolution: {integrity: sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -9418,6 +9768,12 @@ packages: resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} engines: {node: '>=0.6'} + ts-api-utils@2.4.0: + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -9461,8 +9817,8 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - turbo@2.9.14: - resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} + turbo@2.9.6: + resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} hasBin: true type-check@0.4.0: @@ -9737,16 +10093,15 @@ packages: yaml: optional: true - vite@8.0.10: - resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.1.0 - esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 + lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -9757,14 +10112,12 @@ packages: peerDependenciesMeta: '@types/node': optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true jiti: optional: true less: optional: true + lightningcss: + optional: true sass: optional: true sass-embedded: @@ -9875,6 +10228,10 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -9920,6 +10277,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -9932,8 +10301,8 @@ packages: utf-8-validate: optional: true - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9981,6 +10350,11 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + yaml@2.8.4: resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} engines: {node: '>= 14.6'} @@ -10703,7 +11077,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 + browserslist: 4.28.1 lru-cache: 5.1.1 semver: 6.3.1 @@ -11574,13 +11948,6 @@ snapshots: '@codemirror/view': 6.40.0 '@lezer/common': 1.5.1 - '@codemirror/autocomplete@6.20.2': - dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.0 - '@lezer/common': 1.5.2 - '@codemirror/commands@6.10.3': dependencies: '@codemirror/language': 6.12.3 @@ -11680,13 +12047,7 @@ snapshots: '@codemirror/view': 6.40.0 crelt: 1.0.6 - '@codemirror/search@6.7.0': - dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.0 - crelt: 1.0.6 - - '@codemirror/state@6.6.0': + '@codemirror/state@6.6.0': dependencies: '@marijn/find-cluster-break': 1.0.2 @@ -11694,7 +12055,7 @@ snapshots: dependencies: '@codemirror/language': 6.12.3 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.0 + '@codemirror/view': 6.40.0 '@lezer/highlight': 1.2.3 '@codemirror/view@6.40.0': @@ -11704,13 +12065,6 @@ snapshots: style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@codemirror/view@6.43.0': - dependencies: - '@codemirror/state': 6.6.0 - crelt: 1.0.6 - style-mod: 4.1.3 - w3c-keyname: 2.2.8 - '@commitlint/cli@20.4.2(@types/node@25.0.10)(typescript@5.9.3)': dependencies: '@commitlint/format': 20.4.0 @@ -11921,6 +12275,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.9.2': dependencies: tslib: 2.8.1 @@ -12318,7 +12677,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.10.0 + '@emnapi/runtime': 1.8.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -12355,10 +12714,10 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/checkbox@5.1.4(@types/node@20.19.39)': + '@inquirer/checkbox@5.1.5(@types/node@20.19.39)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/figures': 2.0.5 '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: @@ -12383,9 +12742,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/confirm@6.0.12(@types/node@20.19.39)': + '@inquirer/confirm@6.0.13(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12403,11 +12762,11 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/core@11.1.7(@types/node@20.19.39)': + '@inquirer/core@11.1.10(@types/node@20.19.39)': dependencies: - '@inquirer/ansi': 2.0.4 - '@inquirer/figures': 2.0.4 - '@inquirer/type': 4.0.4(@types/node@20.19.39) + '@inquirer/ansi': 2.0.5 + '@inquirer/figures': 2.0.5 + '@inquirer/type': 4.0.5(@types/node@20.19.39) cli-width: 4.1.0 fast-wrap-ansi: 0.2.0 mute-stream: 3.0.0 @@ -12415,11 +12774,11 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/core@11.1.9(@types/node@20.19.39)': + '@inquirer/core@11.1.7(@types/node@20.19.39)': dependencies: - '@inquirer/ansi': 2.0.5 - '@inquirer/figures': 2.0.5 - '@inquirer/type': 4.0.5(@types/node@20.19.39) + '@inquirer/ansi': 2.0.4 + '@inquirer/figures': 2.0.4 + '@inquirer/type': 4.0.4(@types/node@20.19.39) cli-width: 4.1.0 fast-wrap-ansi: 0.2.0 mute-stream: 3.0.0 @@ -12458,9 +12817,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/editor@5.1.1(@types/node@20.19.39)': + '@inquirer/editor@5.1.2(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/external-editor': 3.0.0(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: @@ -12481,9 +12840,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/expand@5.0.13(@types/node@20.19.39)': + '@inquirer/expand@5.0.14(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12541,9 +12900,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/input@5.0.12(@types/node@20.19.39)': + '@inquirer/input@5.0.13(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12562,9 +12921,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/number@4.0.12(@types/node@20.19.39)': + '@inquirer/number@4.0.13(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12585,10 +12944,10 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/password@5.0.12(@types/node@20.19.39)': + '@inquirer/password@5.0.13(@types/node@20.19.39)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12623,18 +12982,18 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/prompts@8.4.2(@types/node@20.19.39)': - dependencies: - '@inquirer/checkbox': 5.1.4(@types/node@20.19.39) - '@inquirer/confirm': 6.0.12(@types/node@20.19.39) - '@inquirer/editor': 5.1.1(@types/node@20.19.39) - '@inquirer/expand': 5.0.13(@types/node@20.19.39) - '@inquirer/input': 5.0.12(@types/node@20.19.39) - '@inquirer/number': 4.0.12(@types/node@20.19.39) - '@inquirer/password': 5.0.12(@types/node@20.19.39) - '@inquirer/rawlist': 5.2.8(@types/node@20.19.39) - '@inquirer/search': 4.1.8(@types/node@20.19.39) - '@inquirer/select': 5.1.4(@types/node@20.19.39) + '@inquirer/prompts@8.4.3(@types/node@20.19.39)': + dependencies: + '@inquirer/checkbox': 5.1.5(@types/node@20.19.39) + '@inquirer/confirm': 6.0.13(@types/node@20.19.39) + '@inquirer/editor': 5.1.2(@types/node@20.19.39) + '@inquirer/expand': 5.0.14(@types/node@20.19.39) + '@inquirer/input': 5.0.13(@types/node@20.19.39) + '@inquirer/number': 4.0.13(@types/node@20.19.39) + '@inquirer/password': 5.0.13(@types/node@20.19.39) + '@inquirer/rawlist': 5.2.9(@types/node@20.19.39) + '@inquirer/search': 4.1.9(@types/node@20.19.39) + '@inquirer/select': 5.1.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12653,9 +13012,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/rawlist@5.2.8(@types/node@20.19.39)': + '@inquirer/rawlist@5.2.9(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: '@types/node': 20.19.39 @@ -12677,9 +13036,9 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/search@4.1.8(@types/node@20.19.39)': + '@inquirer/search@4.1.9(@types/node@20.19.39)': dependencies: - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/figures': 2.0.5 '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: @@ -12712,10 +13071,10 @@ snapshots: optionalDependencies: '@types/node': 20.19.39 - '@inquirer/select@5.1.4(@types/node@20.19.39)': + '@inquirer/select@5.1.5(@types/node@20.19.39)': dependencies: '@inquirer/ansi': 2.0.5 - '@inquirer/core': 11.1.9(@types/node@20.19.39) + '@inquirer/core': 11.1.10(@types/node@20.19.39) '@inquirer/figures': 2.0.5 '@inquirer/type': 4.0.5(@types/node@20.19.39) optionalDependencies: @@ -12779,8 +13138,6 @@ snapshots: '@lezer/common@1.5.1': {} - '@lezer/common@1.5.2': {} - '@lezer/css@1.3.0': dependencies: '@lezer/common': 1.5.1 @@ -12915,6 +13272,99 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@module-federation/dts-plugin@2.3.1(debug@4.4.3)(typescript@5.9.3)': + dependencies: + '@module-federation/error-codes': 2.3.1 + '@module-federation/managers': 2.3.1 + '@module-federation/sdk': 2.3.1 + '@module-federation/third-party-dts-extractor': 2.3.1 + adm-zip: 0.5.16 + ansi-colors: 4.1.3 + axios: 1.13.5(debug@4.4.3) + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0(ws@8.18.0) + node-schedule: 2.1.1 + typescript: 5.9.3 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - debug + - node-fetch + - utf-8-validate + + '@module-federation/error-codes@2.3.1': {} + + '@module-federation/error-codes@2.3.3': {} + + '@module-federation/managers@2.3.1': + dependencies: + '@module-federation/sdk': 2.3.1 + find-pkg: 2.0.0 + fs-extra: 9.1.0 + transitivePeerDependencies: + - node-fetch + + '@module-federation/runtime-core@2.3.1': + dependencies: + '@module-federation/error-codes': 2.3.1 + '@module-federation/sdk': 2.3.1 + transitivePeerDependencies: + - node-fetch + + '@module-federation/runtime-core@2.3.3': + dependencies: + '@module-federation/error-codes': 2.3.3 + '@module-federation/sdk': 2.3.3 + transitivePeerDependencies: + - node-fetch + + '@module-federation/runtime@2.3.1': + dependencies: + '@module-federation/error-codes': 2.3.1 + '@module-federation/runtime-core': 2.3.1 + '@module-federation/sdk': 2.3.1 + transitivePeerDependencies: + - node-fetch + + '@module-federation/runtime@2.3.3': + dependencies: + '@module-federation/error-codes': 2.3.3 + '@module-federation/runtime-core': 2.3.3 + '@module-federation/sdk': 2.3.3 + transitivePeerDependencies: + - node-fetch + + '@module-federation/sdk@2.3.1': {} + + '@module-federation/sdk@2.3.3': {} + + '@module-federation/third-party-dts-extractor@2.3.1': + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + + '@module-federation/vite@1.14.0(debug@4.4.3)(rollup@4.60.2)(typescript@5.9.3)(vite@7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': + dependencies: + '@module-federation/dts-plugin': 2.3.1(debug@4.4.3)(typescript@5.9.3) + '@module-federation/runtime': 2.3.1 + '@module-federation/sdk': 2.3.1 + '@rollup/pluginutils': 5.3.0(rollup@4.60.2) + defu: 6.1.7 + es-module-lexer: 2.0.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + pathe: 2.0.3 + vite: 7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + transitivePeerDependencies: + - bufferutil + - debug + - node-fetch + - rollup + - typescript + - utf-8-validate + - vue-tsc + '@mswjs/interceptors@0.41.2': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -13117,7 +13567,7 @@ snapshots: wordwrap: 1.0.0 wrap-ansi: 7.0.0 - '@oclif/core@4.11.0': + '@oclif/core@4.11.3': dependencies: ansi-escapes: 4.3.2 ansis: 3.17.0 @@ -13130,7 +13580,7 @@ snapshots: is-wsl: 2.2.0 lilconfig: 3.1.3 minimatch: 10.2.5 - semver: 7.7.4 + semver: 7.8.0 string-width: 4.2.3 supports-color: 8.1.1 tinyglobby: 0.2.16 @@ -13163,6 +13613,10 @@ snapshots: dependencies: '@oclif/core': 4.10.6 + '@oclif/plugin-help@6.2.49': + dependencies: + '@oclif/core': 4.11.3 + '@oclif/plugin-not-found@3.2.81(@types/node@20.19.39)': dependencies: '@inquirer/prompts': 7.10.1(@types/node@20.19.39) @@ -13709,78 +14163,153 @@ snapshots: optionalDependencies: rollup: 4.60.2 + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true + '@rollup/rollup-android-arm64@4.60.1': + optional: true + '@rollup/rollup-android-arm64@4.60.2': optional: true + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + '@rollup/rollup-darwin-arm64@4.60.2': optional: true + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + '@rollup/rollup-darwin-x64@4.60.2': optional: true + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + '@rollup/rollup-freebsd-arm64@4.60.2': optional: true + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + '@rollup/rollup-freebsd-x64@4.60.2': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.60.2': optional: true + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + '@rollup/rollup-linux-arm64-musl@4.60.2': optional: true + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + '@rollup/rollup-linux-loong64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + '@rollup/rollup-linux-loong64-musl@4.60.2': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + '@rollup/rollup-linux-ppc64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + '@rollup/rollup-linux-ppc64-musl@4.60.2': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.60.2': optional: true + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.60.2': optional: true + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + '@rollup/rollup-linux-x64-gnu@4.60.2': optional: true + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + '@rollup/rollup-linux-x64-musl@4.60.2': optional: true + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + '@rollup/rollup-openbsd-x64@4.60.2': optional: true + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + '@rollup/rollup-openharmony-arm64@4.60.2': optional: true + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.60.2': optional: true + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.60.2': optional: true + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + '@rollup/rollup-win32-x64-gnu@4.60.2': optional: true + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true @@ -13857,6 +14386,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@sanity-labs/design-tokens@0.0.2-alpha.2': {} + '@sanity/asset-utils@2.3.0': {} '@sanity/bifur-client@0.4.1': @@ -13873,7 +14404,7 @@ snapshots: '@sanity/blueprints@0.15.2': {} - '@sanity/blueprints@0.17.1': {} + '@sanity/blueprints@0.18.0': {} '@sanity/browserslist-config@1.0.5': {} @@ -13884,7 +14415,7 @@ snapshots: nanoid: 3.3.11 rxjs: 7.8.2 - '@sanity/code-input@7.1.0(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + '@sanity/code-input@7.1.0(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/lint@6.9.2)(@codemirror/search@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/lang-java': 6.0.2 @@ -13904,7 +14435,7 @@ snapshots: '@uiw/codemirror-themes': 4.25.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.40.0) '@uiw/react-codemirror': 4.25.9(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.1)(@codemirror/language@6.12.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.6.0)(@codemirror/state@6.6.0)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.40.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 - sanity: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + sanity: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - '@babel/runtime' @@ -14000,6 +14531,20 @@ snapshots: - react-native-b4a - supports-color + '@sanity/federation@0.1.0-alpha.7(debug@4.4.3)(rollup@4.60.2)(typescript@5.9.3)(vite@7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': + dependencies: + '@module-federation/runtime': 2.3.3 + '@module-federation/vite': 1.14.0(debug@4.4.3)(rollup@4.60.2)(typescript@5.9.3)(vite@7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + vite: 7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + transitivePeerDependencies: + - bufferutil + - debug + - node-fetch + - rollup + - typescript + - utf-8-validate + - vue-tsc + '@sanity/functions@1.3.1': {} '@sanity/generate-help-url@4.0.0': {} @@ -14023,7 +14568,7 @@ snapshots: '@sanity/asset-utils': 2.3.0 '@sanity/client': 7.22.0 '@sanity/generate-help-url': 4.0.0 - '@sanity/mutator': 5.23.0(@types/react@19.2.14) + '@sanity/mutator': 5.20.0(@types/react@19.2.14) debug: 4.4.3(supports-color@8.1.1) get-it: 8.7.2 gunzip-maybe: 1.4.2 @@ -14098,9 +14643,9 @@ snapshots: - supports-color - xstate - '@sanity/migrate@6.1.2(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(xstate@5.30.0)': + '@sanity/migrate@6.1.2(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(xstate@5.30.0)': dependencies: - '@oclif/core': 4.11.0 + '@oclif/core': 4.11.3 '@sanity/cli-core': link:packages/@sanity/cli-core '@sanity/client': 7.22.0 '@sanity/mutate': 0.16.1(xstate@5.30.0) @@ -14141,6 +14686,17 @@ snapshots: optionalDependencies: xstate: 5.30.0 + '@sanity/mutator@5.20.0(@types/react@19.2.14)': + dependencies: + '@sanity/diff-match-patch': 3.2.0 + '@sanity/types': 5.20.0(@types/react@19.2.14) + '@sanity/uuid': 3.0.2 + debug: 4.4.3(supports-color@8.1.1) + lodash-es: 4.18.1 + transitivePeerDependencies: + - '@types/react' + - supports-color + '@sanity/mutator@5.23.0(@types/react@19.2.14)': dependencies: '@sanity/diff-match-patch': 3.2.0 @@ -14285,14 +14841,15 @@ snapshots: '@sanity/prism-groq@1.1.2': {} - '@sanity/runtime-cli@15.0.2(@types/node@20.19.39)(esbuild@0.28.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4)': + '@sanity/runtime-cli@15.1.1(@types/node@20.19.39)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.4)': dependencies: '@architect/hydrate': 5.0.2 '@architect/inventory': 5.0.0 - '@inquirer/prompts': 8.4.2(@types/node@20.19.39) - '@oclif/core': 4.11.0 - '@oclif/plugin-help': 6.2.45 - '@sanity/blueprints': 0.17.1 + '@inquirer/prompts': 8.4.3(@types/node@20.19.39) + '@oclif/core': 4.11.3 + '@oclif/plugin-help': 6.2.49 + '@sanity-labs/design-tokens': 0.0.2-alpha.2 + '@sanity/blueprints': 0.18.0 '@sanity/blueprints-parser': 0.4.0 '@sanity/client': 7.22.0 adm-zip: 0.5.17 @@ -14305,18 +14862,17 @@ snapshots: mime-types: 3.0.2 ora: 9.4.0 tar-stream: 3.2.0 - vite: 8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) - vite-tsconfig-paths: 6.1.1(typescript@5.9.3)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - ws: 8.20.0 + vite: 7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vite-tsconfig-paths: 6.1.1(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + ws: 8.20.1 xdg-basedir: 5.1.0 transitivePeerDependencies: - '@types/node' - - '@vitejs/devtools' - bare-abort-controller - bare-buffer - bufferutil - - esbuild - less + - lightningcss - react-native-b4a - sass - sass-embedded @@ -14349,7 +14905,7 @@ snapshots: '@sanity/client': 7.22.0 '@sanity/message-protocol': 0.18.2 '@sanity/sdk': 2.8.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) - '@sanity/types': 5.23.0(@types/react@19.2.14) + '@sanity/types': 5.20.0(@types/react@19.2.14) '@types/lodash-es': 4.17.12 groq: 3.88.1-typegen-experimental.0 lodash-es: 4.18.1 @@ -14375,9 +14931,9 @@ snapshots: '@sanity/json-match': 1.0.5 '@sanity/message-protocol': 0.18.2 '@sanity/mutate': 0.12.6 - '@sanity/types': 5.23.0(@types/react@19.2.14) + '@sanity/types': 5.20.0(@types/react@19.2.14) groq: 3.88.1-typegen-experimental.0 - groq-js: 1.30.1 + groq-js: 1.29.0 lodash-es: 4.18.1 reselect: 5.1.1 rxjs: 7.8.2 @@ -14413,6 +14969,12 @@ snapshots: '@actions/github': 9.0.0 yaml: 2.8.4 + '@sanity/types@5.20.0(@types/react@19.2.14)': + dependencies: + '@sanity/client': 7.22.0 + '@sanity/media-library-types': 1.4.0 + '@types/react': 19.2.14 + '@sanity/types@5.23.0(@types/react@19.2.14)': dependencies: '@sanity/client': 7.22.0 @@ -14453,7 +15015,7 @@ snapshots: '@types/uuid': 8.3.4 uuid: 8.3.2 - '@sanity/vision@5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + '@sanity/vision@5.23.0(@babel/runtime@7.28.6)(@codemirror/lint@6.9.2)(@codemirror/theme-one-dark@6.1.3)(@emotion/is-prop-valid@1.4.0)(codemirror@6.0.2)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: '@codemirror/autocomplete': 6.20.1 '@codemirror/commands': 6.10.3 @@ -14479,7 +15041,7 @@ snapshots: react: 19.2.5 react-rx: 4.2.2(react@19.2.5)(rxjs@7.8.2) rxjs: 7.8.2 - sanity: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + sanity: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - '@babel/runtime' @@ -15003,22 +15565,22 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@turbo/darwin-64@2.9.14': + '@turbo/darwin-64@2.9.6': optional: true - '@turbo/darwin-arm64@2.9.14': + '@turbo/darwin-arm64@2.9.6': optional: true - '@turbo/linux-64@2.9.14': + '@turbo/linux-64@2.9.6': optional: true - '@turbo/linux-arm64@2.9.14': + '@turbo/linux-arm64@2.9.6': optional: true - '@turbo/windows-64@2.9.14': + '@turbo/windows-64@2.9.6': optional: true - '@turbo/windows-arm64@2.9.14': + '@turbo/windows-arm64@2.9.6': optional: true '@tybys/wasm-util@0.10.1': @@ -15082,7 +15644,7 @@ snapshots: dependencies: '@types/node': 25.0.10 '@types/tough-cookie': 4.0.5 - parse5: 8.0.1 + parse5: 8.0.0 undici-types: 7.22.0 '@types/jsesc@2.5.1': {} @@ -15214,8 +15776,8 @@ snapshots: '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) - '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 debug: 4.4.3(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: @@ -15244,6 +15806,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -15262,6 +15828,8 @@ snapshots: '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.57.0': {} + '@typescript-eslint/types@8.59.0': {} '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': @@ -15274,7 +15842,7 @@ snapshots: minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.16 - ts-api-utils: 2.5.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -15504,7 +16072,7 @@ snapshots: magicast: 0.5.2 obug: 2.1.1 tinyrainbow: 3.1.0 - vitest: 4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + vitest: 4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) transitivePeerDependencies: - supports-color @@ -15525,29 +16093,21 @@ snapshots: optionalDependencies: vite: 7.3.2(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': - dependencies: - '@vitest/spy': 4.1.5 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) - - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': + '@vitest/mocker@4.1.5(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vite: 7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) - '@vitest/mocker@4.1.5(vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': + '@vitest/mocker@4.1.5(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4))': dependencies: '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vite: 7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) '@vitest/pretty-format@4.1.5': dependencies: @@ -15688,6 +16248,8 @@ snapshots: acorn@8.16.0: {} + adm-zip@0.5.16: {} + adm-zip@0.5.17: {} agent-base@7.1.4: {} @@ -15781,10 +16343,20 @@ snapshots: asynckit@0.4.0: {} + at-least-node@1.0.0: {} + attr-accept@2.2.5: {} aws4@1.13.2: {} + axios@1.13.5(debug@4.4.3): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3) + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + b4a@1.7.3: {} babel-plugin-macros@3.1.0: @@ -15917,12 +16489,20 @@ snapshots: dependencies: pako: 0.2.9 + browserslist@4.28.1: + dependencies: + baseline-browser-mapping: 2.10.16 + caniuse-lite: 1.0.30001766 + electron-to-chromium: 1.5.278 + node-releases: 2.0.27 + update-browserslist-db: 1.2.3(browserslist@4.28.1) + browserslist@4.28.2: dependencies: baseline-browser-mapping: 2.10.16 - caniuse-lite: 1.0.30001790 - electron-to-chromium: 1.5.344 - node-releases: 2.0.38 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.340 + node-releases: 2.0.37 update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer-crc32@0.2.13: {} @@ -15997,7 +16577,9 @@ snapshots: camelize@1.0.1: optional: true - caniuse-lite@1.0.30001790: {} + caniuse-lite@1.0.30001766: {} + + caniuse-lite@1.0.30001788: {} capital-case@1.0.4: dependencies: @@ -16110,13 +16692,13 @@ snapshots: codemirror@6.0.2: dependencies: - '@codemirror/autocomplete': 6.20.2 + '@codemirror/autocomplete': 6.20.1 '@codemirror/commands': 6.10.3 '@codemirror/language': 6.12.3 '@codemirror/lint': 6.9.2 - '@codemirror/search': 6.7.0 + '@codemirror/search': 6.6.0 '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.0 + '@codemirror/view': 6.40.0 color-convert@2.0.1: dependencies: @@ -16200,7 +16782,7 @@ snapshots: core-js-compat@3.48.0: dependencies: - browserslist: 4.28.2 + browserslist: 4.28.1 core-util-is@1.0.3: {} @@ -16230,6 +16812,10 @@ snapshots: crelt@1.0.6: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -16364,6 +16950,8 @@ snapshots: define-lazy-prop@3.0.0: {} + defu@6.1.7: {} + delayed-stream@1.0.0: {} detect-indent@6.1.0: {} @@ -16443,7 +17031,9 @@ snapshots: dependencies: jake: 10.9.4 - electron-to-chromium@1.5.344: {} + electron-to-chromium@1.5.278: {} + + electron-to-chromium@1.5.340: {} emoji-regex@10.6.0: {} @@ -16581,8 +17171,8 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 - is-core-module: 2.16.2 - resolve: 1.22.12 + is-core-module: 2.16.1 + resolve: 1.22.11 transitivePeerDependencies: - supports-color optional: true @@ -16612,7 +17202,7 @@ snapshots: eslint-plugin-import-x@4.16.2(@typescript-eslint/utils@8.59.0(eslint@10.2.1(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@10.2.1(jiti@2.7.0)): dependencies: '@package-json/types': 0.0.12 - '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/types': 8.57.0 comment-parser: 1.4.5 debug: 4.4.3(supports-color@8.1.1) eslint: 10.2.1(jiti@2.7.0) @@ -16818,6 +17408,10 @@ snapshots: exif-component@1.0.1: {} + expand-tilde@2.0.2: + dependencies: + homedir-polyfill: 1.0.3 + expect-type@1.3.0: {} ext-list@2.2.2: @@ -16936,6 +17530,14 @@ snapshots: dependencies: user-home: 2.0.0 + find-file-up@2.0.1: + dependencies: + resolve-dir: 1.0.1 + + find-pkg@2.0.0: + dependencies: + find-file-up: 2.0.1 + find-root@1.1.0: {} find-up-simple@1.0.1: {} @@ -16974,6 +17576,10 @@ snapshots: dependencies: tslib: 2.8.1 + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) + form-data-encoder@2.1.4: {} form-data-encoder@4.1.0: {} @@ -17018,6 +17624,13 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fsevents@2.3.3: optional: true @@ -17076,6 +17689,10 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -17117,6 +17734,20 @@ snapshots: dependencies: ini: 4.1.1 + global-modules@1.0.0: + dependencies: + global-prefix: 1.0.2 + is-windows: 1.0.2 + resolve-dir: 1.0.1 + + global-prefix@1.0.2: + dependencies: + expand-tilde: 2.0.2 + homedir-polyfill: 1.0.3 + ini: 1.3.8 + is-windows: 1.0.2 + which: 1.3.1 + globals@15.15.0: {} globals@16.5.0: {} @@ -17176,6 +17807,12 @@ snapshots: graceful-fs@4.2.11: {} + groq-js@1.29.0: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + groq-js@1.30.1: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -17213,11 +17850,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.3: - dependencies: - function-bind: 1.1.2 - optional: true - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -17247,6 +17879,10 @@ snapshots: dependencies: react-is: 16.13.1 + homedir-polyfill@1.0.3: + dependencies: + parse-passwd: 1.0.0 + hosted-git-info@7.0.2: dependencies: lru-cache: 10.4.3 @@ -17391,11 +18027,6 @@ snapshots: dependencies: hasown: 2.0.2 - is-core-module@2.16.2: - dependencies: - hasown: 2.0.3 - optional: true - is-decimal@2.0.1: {} is-deflate@1.0.0: {} @@ -17520,6 +18151,10 @@ snapshots: - supports-color - utf-8-validate + isomorphic-ws@5.0.0(ws@8.18.0): + dependencies: + ws: 8.18.0 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -17541,6 +18176,8 @@ snapshots: javascript-stringify@2.1.0: {} + jiti@2.6.1: {} + jiti@2.7.0: {} jju@1.4.0: {} @@ -17595,7 +18232,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - parse5: 8.0.1 + parse5: 8.0.0 saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.1 @@ -17705,7 +18342,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) formatly: 0.3.0 get-tsconfig: 4.14.0 - jiti: 2.7.0 + jiti: 2.6.1 minimist: 1.2.8 oxc-parser: 0.127.0 oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -17714,7 +18351,7 @@ snapshots: strip-json-comments: 5.0.3 tinyglobby: 0.2.16 unbash: 3.0.0 - yaml: 2.8.4 + yaml: 2.8.3 zod: 4.3.6 transitivePeerDependencies: - '@emnapi/core' @@ -17793,7 +18430,7 @@ snapshots: picomatch: 4.0.4 string-argv: 0.3.2 tinyexec: 1.0.4 - yaml: 2.8.4 + yaml: 2.8.3 listr2@9.0.5: dependencies: @@ -17850,6 +18487,8 @@ snapshots: strip-ansi: 7.1.2 wrap-ansi: 9.0.2 + long-timeout@0.1.1: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -17870,6 +18509,8 @@ snapshots: dependencies: yallist: 3.1.1 + luxon@3.7.2: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -18055,7 +18696,7 @@ snapshots: '@next/env': 16.2.3 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.16 - caniuse-lite: 1.0.30001790 + caniuse-lite: 1.0.30001766 postcss: 8.4.31 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) @@ -18097,7 +18738,15 @@ snapshots: dependencies: node-addon-api: 7.1.1 - node-releases@2.0.38: {} + node-releases@2.0.27: {} + + node-releases@2.0.37: {} + + node-schedule@2.1.1: + dependencies: + cron-parser: 4.9.0 + long-timeout: 0.1.1 + sorted-array-functions: 1.3.0 normalize-package-data@6.0.2: dependencies: @@ -18410,6 +19059,8 @@ snapshots: parse-ms@4.0.0: {} + parse-passwd@1.0.0: {} + parse-path@7.1.0: dependencies: protocols: 2.0.2 @@ -18423,6 +19074,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + parse5@8.0.1: dependencies: entities: 8.0.0 @@ -18523,12 +19178,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.13: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -18574,6 +19223,8 @@ snapshots: protocols@2.0.2: {} + proxy-from-env@1.1.0: {} + publint@0.3.18: dependencies: '@publint/pack': 0.1.4 @@ -18850,6 +19501,11 @@ snapshots: resolve-alpn@1.2.1: {} + resolve-dir@1.0.1: + dependencies: + expand-tilde: 2.0.2 + global-modules: 1.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -18862,13 +19518,11 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.12: + resolve@1.22.8: dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 + is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - optional: true responselike@3.0.0: dependencies: @@ -18944,6 +19598,37 @@ snapshots: transitivePeerDependencies: - supports-color + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 @@ -19005,7 +19690,7 @@ snapshots: safer-buffer@2.1.2: {} - sanity-plugin-media@4.1.1(@emotion/is-prop-valid@1.4.0)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)): + sanity-plugin-media@4.1.1(@emotion/is-prop-valid@1.4.0)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react-is@19.2.4)(react@19.2.5)(sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3))(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)): dependencies: '@hookform/resolvers': 3.10.0(react-hook-form@7.71.2(react@19.2.5)) '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5) @@ -19035,7 +19720,7 @@ snapshots: redux: 5.0.1 redux-observable: 3.0.0-rc.2(redux@5.0.1)(rxjs@7.8.2) rxjs: 7.8.2 - sanity: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) + sanity: 5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3) styled-components: 6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) zod: 3.25.76 transitivePeerDependencies: @@ -19164,7 +19849,7 @@ snapshots: - typescript - utf-8-validate - sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): + sanity@5.23.0(@emotion/is-prop-valid@1.4.0)(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(styled-components@6.4.0(css-to-react-native@3.2.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@5.9.3): dependencies: '@algorithm.ts/lcs': 4.0.5 '@date-fns/tz': 1.4.1 @@ -19203,7 +19888,7 @@ snapshots: '@sanity/logos': 2.2.2(react@19.2.5) '@sanity/media-library-types': 1.4.0 '@sanity/message-protocol': 0.23.0 - '@sanity/migrate': 6.1.2(@oclif/core@4.11.0)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(xstate@5.30.0) + '@sanity/migrate': 6.1.2(@oclif/core@4.11.3)(@sanity/cli-core@packages+@sanity+cli-core)(@types/react@19.2.14)(xstate@5.30.0) '@sanity/mutate': 0.16.1(xstate@5.30.0) '@sanity/mutator': 5.23.0(@types/react@19.2.14) '@sanity/presentation-comlink': 2.0.1(@sanity/client@7.22.0)(@sanity/types@5.23.0(@types/react@19.2.14)) @@ -19313,6 +19998,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + sentence-case@3.0.4: dependencies: no-case: 3.0.4 @@ -19423,6 +20110,8 @@ snapshots: sort-object-keys: 1.1.3 tinyglobby: 0.2.16 + sorted-array-functions@1.3.0: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -19725,6 +20414,10 @@ snapshots: treeify@1.1.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -19751,7 +20444,7 @@ snapshots: tsx@4.21.0: dependencies: esbuild: 0.27.4 - get-tsconfig: 4.14.0 + get-tsconfig: 4.13.7 optionalDependencies: fsevents: 2.3.3 @@ -19761,14 +20454,14 @@ snapshots: tunnel@0.0.6: {} - turbo@2.9.14: + turbo@2.9.6: optionalDependencies: - '@turbo/darwin-64': 2.9.14 - '@turbo/darwin-arm64': 2.9.14 - '@turbo/linux-64': 2.9.14 - '@turbo/linux-arm64': 2.9.14 - '@turbo/windows-64': 2.9.14 - '@turbo/windows-arm64': 2.9.14 + '@turbo/darwin-64': 2.9.6 + '@turbo/darwin-arm64': 2.9.6 + '@turbo/linux-64': 2.9.6 + '@turbo/linux-arm64': 2.9.6 + '@turbo/windows-64': 2.9.6 + '@turbo/windows-arm64': 2.9.6 type-check@0.4.0: dependencies: @@ -19891,6 +20584,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + update-browserslist-db@1.2.3(browserslist@4.28.1): + dependencies: + browserslist: 4.28.1 + escalade: 3.2.0 + picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -20011,12 +20710,12 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): + vite-tsconfig-paths@6.1.1(typescript@5.9.3)(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) - vite: 8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vite: 7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) transitivePeerDependencies: - supports-color - typescript @@ -20027,7 +20726,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.6 - rollup: 4.60.2 + rollup: 4.60.1 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 20.19.39 @@ -20044,7 +20743,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 postcss: 8.5.6 - rollup: 4.60.2 + rollup: 4.60.1 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.0.10 @@ -20055,50 +20754,36 @@ snapshots: tsx: 4.21.0 yaml: 2.8.4 - vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4): + vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4): dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.13 - rolldown: 1.0.0-rc.17 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 20.19.39 esbuild: 0.27.4 - fsevents: 2.3.3 - jiti: 2.7.0 - terser: 5.46.0 - tsx: 4.21.0 - yaml: 2.8.4 - - vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4): - dependencies: - lightningcss: 1.32.0 + fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.13 - rolldown: 1.0.0-rc.17 + postcss: 8.5.6 + rollup: 4.60.2 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 20.19.39 - esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.7.0 + lightningcss: 1.32.0 terser: 5.46.0 tsx: 4.21.0 yaml: 2.8.4 - vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4): + vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4): dependencies: - lightningcss: 1.32.0 + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.13 - rolldown: 1.0.0-rc.17 + postcss: 8.5.6 + rollup: 4.60.2 tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.0.10 - esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.7.0 + lightningcss: 1.32.0 terser: 5.46.0 tsx: 4.21.0 yaml: 2.8.4 @@ -20132,10 +20817,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): + vitest@4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + '@vitest/mocker': 4.1.5(vite@7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -20152,7 +20837,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.10(@types/node@20.19.39)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vite: 7.3.3(@types/node@20.19.39)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.39 @@ -20161,10 +20846,10 @@ snapshots: transitivePeerDependencies: - msw - vitest@4.1.5(@types/node@20.19.39)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): + vitest@4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): dependencies: '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) + '@vitest/mocker': 4.1.5(vite@7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) '@vitest/pretty-format': 4.1.5 '@vitest/runner': 4.1.5 '@vitest/snapshot': 4.1.5 @@ -20181,36 +20866,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 8.0.10(@types/node@20.19.39)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 20.19.39 - '@vitest/coverage-istanbul': 4.1.5(vitest@4.1.5) - jsdom: 29.1.1(@noble/hashes@2.0.1) - transitivePeerDependencies: - - msw - - vitest@4.1.5(@types/node@25.0.10)(@vitest/coverage-istanbul@4.1.5)(jsdom@29.1.1(@noble/hashes@2.0.1))(vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)): - dependencies: - '@vitest/expect': 4.1.5 - '@vitest/mocker': 4.1.5(vite@8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4)) - '@vitest/pretty-format': 4.1.5 - '@vitest/runner': 4.1.5 - '@vitest/snapshot': 4.1.5 - '@vitest/spy': 4.1.5 - '@vitest/utils': 4.1.5 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.0.4 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 8.0.10(@types/node@25.0.10)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) + vite: 7.3.3(@types/node@25.0.10)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.4) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.0.10 @@ -20263,6 +20919,10 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + which@1.3.1: + dependencies: + isexe: 2.0.0 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -20308,9 +20968,11 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + ws@8.19.0: {} - ws@8.20.0: {} + ws@8.20.1: {} wsl-utils@0.3.1: dependencies: @@ -20335,6 +20997,8 @@ snapshots: yaml@1.10.2: {} + yaml@2.8.3: {} + yaml@2.8.4: {} yargs-parser@21.1.1: {}