From 6975da35ca1c3c543336dbaeced97a2f45ee99c3 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 11:11:39 +0200 Subject: [PATCH 1/9] feat: persist Metro transform and file-map caches under .harness Introduce @react-native-harness/cache as the single owner of .harness/cache layout and warmth checks. Metro's transform cache moves to .harness/cache/metro and its file-map (haste) cache, previously left in $TMPDIR, is now pinned to .harness/cache/metro-file-map. Both are enabled by default via a new `cache` config option group; `unstable__enableMetroCache` is kept as a deprecated alias for one release. The GitHub Action's cache step now covers both directories under a bumped cache key. --- .gitignore | 1 - .nx/version-plans/persist-metro-cache.md | 5 + action.yml | 10 +- actions/shared/index.cjs | 21 ++- packages/bundler-metro/package.json | 1 + .../bundler-metro/src/__tests__/paths.test.ts | 55 +------- .../src/__tests__/withRnHarness.test.ts | 119 +++++++++++++++++ packages/bundler-metro/src/index.ts | 1 - packages/bundler-metro/src/metro-cache.ts | 27 ++-- packages/bundler-metro/src/paths.ts | 22 --- packages/bundler-metro/src/withRnHarness.ts | 40 +++++- packages/bundler-metro/tsconfig.json | 3 + packages/bundler-metro/tsconfig.lib.json | 3 + packages/cache/README.md | 26 ++++ packages/cache/eslint.config.mjs | 20 +++ packages/cache/package.json | 22 +++ packages/cache/src/__tests__/boundary.test.ts | 95 +++++++++++++ packages/cache/src/__tests__/index.test.ts | 54 ++++++++ packages/cache/src/__tests__/paths.test.ts | 18 +++ packages/cache/src/__tests__/warmth.test.ts | 47 +++++++ packages/cache/src/index.ts | 42 ++++++ packages/cache/src/paths.ts | 43 ++++++ packages/cache/src/warmth.ts | 59 ++++++++ packages/cache/tsconfig.json | 13 ++ packages/cache/tsconfig.lib.json | 18 +++ packages/cache/vite.config.ts | 18 +++ .../resolveMetroCacheEnabled.test.ts | 126 ++++++++++++++++++ packages/config/src/index.ts | 1 + packages/config/src/types.ts | 66 ++++++++- packages/github-action/README.md | 2 +- packages/github-action/src/action.yml | 10 +- packages/jest/package.json | 1 + .../jest/src/__tests__/harness-cache.test.ts | 91 +++++++++++++ packages/jest/src/harness-session.ts | 41 +++++- packages/jest/tsconfig.json | 3 + packages/jest/tsconfig.lib.json | 3 + pnpm-lock.yaml | 15 +++ tsconfig.json | 3 + .../docs/getting-started/configuration.mdx | 5 +- website/src/docs/guides/ci-cd.md | 4 +- 40 files changed, 1035 insertions(+), 119 deletions(-) create mode 100644 .nx/version-plans/persist-metro-cache.md create mode 100644 packages/cache/README.md create mode 100644 packages/cache/eslint.config.mjs create mode 100644 packages/cache/package.json create mode 100644 packages/cache/src/__tests__/boundary.test.ts create mode 100644 packages/cache/src/__tests__/index.test.ts create mode 100644 packages/cache/src/__tests__/paths.test.ts create mode 100644 packages/cache/src/__tests__/warmth.test.ts create mode 100644 packages/cache/src/index.ts create mode 100644 packages/cache/src/paths.ts create mode 100644 packages/cache/src/warmth.ts create mode 100644 packages/cache/tsconfig.json create mode 100644 packages/cache/tsconfig.lib.json create mode 100644 packages/cache/vite.config.ts create mode 100644 packages/config/src/__tests__/resolveMetroCacheEnabled.test.ts create mode 100644 packages/jest/src/__tests__/harness-cache.test.ts diff --git a/.gitignore b/.gitignore index a3957e87..d4eef0ab 100644 --- a/.gitignore +++ b/.gitignore @@ -128,4 +128,3 @@ npm-debug.* *.mobileprovision *.orig.* web-build/ -cache/ diff --git a/.nx/version-plans/persist-metro-cache.md b/.nx/version-plans/persist-metro-cache.md new file mode 100644 index 00000000..3493d15f --- /dev/null +++ b/.nx/version-plans/persist-metro-cache.md @@ -0,0 +1,5 @@ +--- +__default__: minor +--- + +Metro's transform and file-map caches now persist under `.harness/cache/metro` and `.harness/cache/metro-file-map` in your project root, enabled by default, so repeated Metro runs and CI jobs reuse work instead of rebuilding from scratch. Configure this with the new `cache.metro` and `cache.version` options; the previous `unstable__enableMetroCache` flag still works but is deprecated in favor of `cache.metro`. The official GitHub Action restores and saves the new cache paths automatically. diff --git a/action.yml b/action.yml index f9098959..09cf8899 100644 --- a/action.yml +++ b/action.yml @@ -64,13 +64,15 @@ runs: echo "Please provide the path to the built app (.apk for Android, .app for iOS)" exit 1 fi - - name: Metro bundler cache (.harness/metro-cache) + - name: Metro bundler cache (.harness/cache/metro) uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ steps.load-config.outputs.projectRoot }}/.harness/metro-cache - key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }} + path: | + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro-file-map + key: ${{ runner.os }}-metro-cache-v2-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }} restore-keys: | - ${{ runner.os }}-metro-cache- + ${{ runner.os }}-metro-cache-v2- - name: Restore Harness cache id: cache-harness-restore if: fromJson(steps.load-config.outputs.config).platformId == 'ios' diff --git a/actions/shared/index.cjs b/actions/shared/index.cjs index e05c4ad4..861e89fa 100644 --- a/actions/shared/index.cjs +++ b/actions/shared/index.cjs @@ -4426,6 +4426,7 @@ var pluginsLogger = logger.child("plugins"); // ../config/dist/types.js var DEFAULT_METRO_PORT = 8081; +var configLogger = logger.child("config"); var RunnerSchema = external_exports.object({ name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), config: external_exports.record(external_exports.any()), @@ -4449,9 +4450,23 @@ var ConfigSchema = external_exports.object({ bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(6e4), maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2), eagerPrewarm: external_exports.boolean().optional().default(true).describe("Start building the Metro bundle while the platform (emulator, simulator, or browser) is still booting, so the first bundle is ready sooner. Disable to defer the first bundle build until app startup."), - resetEnvironmentBetweenTestFiles: external_exports.boolean().optional().default(true), - unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().default(false), - unstable__enableMetroCache: external_exports.boolean().optional().default(false), + resetEnvironmentBetweenTestFiles: external_exports.union([external_exports.boolean(), external_exports.enum(["process", "runtime"])]).optional().default(true).describe("Controls how the environment is reset between test files. `true` (default) and `'process'` kill and cold-restart the app process. `'runtime'` reloads the JS runtime in place (DevSettings.reload() / window.location.reload()), which is cheaper but escalates to a process restart if the reload fails or the app does not reconnect in time. `false` disables resetting the environment between test files entirely."), + skipAlreadyIncludedModules: external_exports.boolean().optional().describe('Skip re-sending modules already served in the main app bundle when Metro serves a per-test-file bundle. Defaults to true; set to false as an escape hatch if it causes issues. Left undefined (rather than defaulted here) so `resolveSkipAlreadyIncludedModules` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__skipAlreadyIncludedModules` alias.'), + // Deprecated alias for `skipAlreadyIncludedModules` -- see + // `resolveSkipAlreadyIncludedModules`. Left without a `.default()` for the + // same reason: a default value would be indistinguishable from the user + // never having set it, which would break alias-vs-explicit-flag precedence. + unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().describe("Deprecated. Use `skipAlreadyIncludedModules` instead."), + cache: external_exports.object({ + metro: external_exports.boolean().optional().describe('Enable persistent Metro transform and file-map caching under the .harness cache directory. Defaults to true; set to false to always start with a cold Metro cache. Left undefined (rather than defaulted here) so `resolveMetroCacheEnabled` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__enableMetroCache` alias.'), + version: external_exports.string().optional().describe("User-controlled salt folded into the Metro cacheVersion. Bump it to force-invalidate previously cached transforms.") + }).optional(), + // Deprecated alias for `cache.metro` -- see `resolveMetroCacheEnabled`. + // Left without a `.default()` for the same reason as + // `unstable__skipAlreadyIncludedModules`: a default value would be + // indistinguishable from the user never having set it, which would break + // alias-vs-explicit-flag precedence. + unstable__enableMetroCache: external_exports.boolean().optional().describe("Deprecated. Use `cache.metro` instead."), permissions: external_exports.boolean().optional().default(false).describe("Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent."), detectNativeCrashes: external_exports.boolean().optional().default(true), crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), diff --git a/packages/bundler-metro/package.json b/packages/bundler-metro/package.json index adc1b124..f207ca55 100644 --- a/packages/bundler-metro/package.json +++ b/packages/bundler-metro/package.json @@ -18,6 +18,7 @@ "dependencies": { "@react-native/metro-config": "*", "@react-native-harness/babel-preset": "workspace:*", + "@react-native-harness/cache": "workspace:*", "@react-native-harness/tools": "workspace:*", "@react-native-harness/config": "workspace:*", "@react-native-harness/runtime": "workspace:*", diff --git a/packages/bundler-metro/src/__tests__/paths.test.ts b/packages/bundler-metro/src/__tests__/paths.test.ts index 80300e29..3e7539b4 100644 --- a/packages/bundler-metro/src/__tests__/paths.test.ts +++ b/packages/bundler-metro/src/__tests__/paths.test.ts @@ -1,33 +1,10 @@ -import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; -import { afterEach, describe, expect, it } from 'vitest'; -import { - getHarnessManifestPath, - getHarnessMetroCachePath, - getHarnessRootPath, - isMetroCacheReusable, -} from '../paths.js'; - -const tempDirs: string[] = []; - -const createTempProjectRoot = (): string => { - const tempDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'rn-harness-bundler-metro-') - ); - tempDirs.push(tempDir); - return tempDir; -}; - -afterEach(() => { - for (const tempDir of tempDirs.splice(0)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } -}); +import { describe, expect, it } from 'vitest'; +import { getHarnessManifestPath, getHarnessRootPath } from '../paths.js'; describe('bundler metro paths', () => { it('resolves the harness root under the project root', () => { - const projectRoot = createTempProjectRoot(); + const projectRoot = '/tmp/some-project'; expect(getHarnessRootPath(projectRoot)).toBe( path.join(projectRoot, '.harness') @@ -35,31 +12,5 @@ describe('bundler metro paths', () => { expect(getHarnessManifestPath(projectRoot)).toBe( path.join(projectRoot, '.harness', 'manifest.js') ); - expect(getHarnessMetroCachePath(projectRoot)).toBe( - path.join(projectRoot, '.harness', 'metro-cache') - ); - }); - - it('returns false when the metro cache directory is missing', () => { - const projectRoot = createTempProjectRoot(); - - expect(isMetroCacheReusable(projectRoot)).toBe(false); - }); - - it('returns false when the metro cache directory is empty', () => { - const projectRoot = createTempProjectRoot(); - fs.mkdirSync(getHarnessMetroCachePath(projectRoot), { recursive: true }); - - expect(isMetroCacheReusable(projectRoot)).toBe(false); - }); - - it('returns true when the metro cache directory contains entries', () => { - const projectRoot = createTempProjectRoot(); - const metroCachePath = getHarnessMetroCachePath(projectRoot); - - fs.mkdirSync(metroCachePath, { recursive: true }); - fs.writeFileSync(path.join(metroCachePath, 'entry'), 'cached'); - - expect(isMetroCacheReusable(projectRoot)).toBe(true); }); }); diff --git a/packages/bundler-metro/src/__tests__/withRnHarness.test.ts b/packages/bundler-metro/src/__tests__/withRnHarness.test.ts index 64cd2cb0..ed1aebaf 100644 --- a/packages/bundler-metro/src/__tests__/withRnHarness.test.ts +++ b/packages/bundler-metro/src/__tests__/withRnHarness.test.ts @@ -1,9 +1,14 @@ import os from 'node:os'; import { describe, expect, it, vi } from 'vitest'; +import { getConfig } from '@react-native-harness/config'; type MinimalMetroConfig = { projectRoot: string; maxWorkers?: number; + cacheVersion?: string; + cacheStores?: unknown; + fileMapCacheDirectory?: string; + hasteMapCacheDirectory?: string; serializer?: { isThirdPartyModule?: (module: { path: string }) => boolean; }; @@ -14,10 +19,27 @@ type MinimalMetroConfig = { }; }; +const { ensureDomainDirectories } = vi.hoisted(() => ({ + ensureDomainDirectories: vi.fn(), +})); + vi.mock('@react-native-harness/config', async (importOriginal) => ({ ...(await importOriginal()), getConfig: vi.fn(async () => ({ config: {}, + projectRoot: '/tmp/app', + })), +})); + +vi.mock('@react-native-harness/cache', () => ({ + createHarnessCache: vi.fn((options: { projectRoot: string }) => ({ + paths: { + root: `${options.projectRoot}/.harness/cache`, + metroTransform: `${options.projectRoot}/.harness/cache/metro`, + metroFileMap: `${options.projectRoot}/.harness/cache/metro-file-map`, + }, + isWarm: vi.fn(() => false), + ensureDomainDirectories, })), })); @@ -128,4 +150,101 @@ describe('withRnHarness', () => { ); expect(config.maxWorkers).toBeLessThan(64); }); + + it('enables the persistent Metro cache by default', async () => { + const { withRnHarness } = await import('../withRnHarness.js'); + const { getHarnessCacheStores } = await import('../metro-cache.js'); + + const config = (await withRnHarness( + { + projectRoot: '/tmp/app', + serializer: {}, + }, + true, + )()) as unknown as MinimalMetroConfig; + + expect(getHarnessCacheStores).toHaveBeenCalledWith( + '/tmp/app/.harness/cache/metro', + ); + expect(config.cacheStores).toBe( + vi.mocked(getHarnessCacheStores).mock.results.at(-1)?.value, + ); + expect(config.fileMapCacheDirectory).toBe( + '/tmp/app/.harness/cache/metro-file-map', + ); + expect(ensureDomainDirectories).toHaveBeenCalledWith('metro'); + expect(config.cacheVersion).toMatch(/^react-native-harness:\d+\.\d+\.\d+/); + }); + + it('respects a user-configured file map cache directory', async () => { + const { withRnHarness } = await import('../withRnHarness.js'); + + const config = (await withRnHarness( + { + projectRoot: '/tmp/app', + fileMapCacheDirectory: '/custom/file-map', + serializer: {}, + }, + true, + )()) as unknown as MinimalMetroConfig; + + expect(config.fileMapCacheDirectory).toBe('/custom/file-map'); + }); + + it('respects the legacy haste map cache directory', async () => { + const { withRnHarness } = await import('../withRnHarness.js'); + + const config = (await withRnHarness( + { + projectRoot: '/tmp/app', + hasteMapCacheDirectory: '/custom/haste-map', + serializer: {}, + }, + true, + )()) as unknown as MinimalMetroConfig; + + expect(config.fileMapCacheDirectory).toBeUndefined(); + expect(config.hasteMapCacheDirectory).toBe('/custom/haste-map'); + }); + + it('leaves Metro cache defaults untouched when caching is disabled', async () => { + const { withRnHarness } = await import('../withRnHarness.js'); + + vi.mocked(getConfig).mockResolvedValueOnce({ + config: { cache: { metro: false } }, + projectRoot: '/tmp/app', + } as Awaited>); + + const config = (await withRnHarness( + { + projectRoot: '/tmp/app', + serializer: {}, + }, + true, + )()) as unknown as MinimalMetroConfig; + + expect(config.cacheStores).toBeUndefined(); + expect(config.fileMapCacheDirectory).toBeUndefined(); + }); + + it('folds the user cache version salt into the Metro cacheVersion', async () => { + const { withRnHarness } = await import('../withRnHarness.js'); + + vi.mocked(getConfig).mockResolvedValueOnce({ + config: { cache: { version: 'my-salt' } }, + projectRoot: '/tmp/app', + } as Awaited>); + + const config = (await withRnHarness( + { + projectRoot: '/tmp/app', + serializer: {}, + }, + true, + )()) as unknown as MinimalMetroConfig; + + expect(config.cacheVersion).toMatch( + /^react-native-harness:\d+\.\d+\.\d+.*:my-salt$/, + ); + }); }); diff --git a/packages/bundler-metro/src/index.ts b/packages/bundler-metro/src/index.ts index 6ad72eeb..de75865b 100644 --- a/packages/bundler-metro/src/index.ts +++ b/packages/bundler-metro/src/index.ts @@ -7,7 +7,6 @@ export type { PrewarmState, } from './types.js'; export type { Reporter, ReportableEvent } from './reporter.js'; -export { isMetroCacheReusable } from './paths.js'; export { StartupStallError, type StartupStallCode, diff --git a/packages/bundler-metro/src/metro-cache.ts b/packages/bundler-metro/src/metro-cache.ts index e625004d..5af44f37 100644 --- a/packages/bundler-metro/src/metro-cache.ts +++ b/packages/bundler-metro/src/metro-cache.ts @@ -1,21 +1,16 @@ -import fs from 'node:fs'; import type { CacheStoresConfigT } from 'metro-config'; import { CacheStore, MetroCache } from 'metro-cache'; import type { MixedOutput, TransformResult } from 'metro'; -import { getHarnessMetroCachePath } from './paths.js'; -export const getHarnessCacheStores = (): (( - metroCache: MetroCache -) => CacheStoresConfigT) => { - return ({ FileStore }) => { - const cacheRoot = getHarnessMetroCachePath(); - - fs.mkdirSync(cacheRoot, { recursive: true }); - - return [ - new FileStore({ root: cacheRoot }) as CacheStore< - TransformResult - >, - ]; - }; +// Directory creation is handled by the cache package's +// ensureDomainDirectories; FileStore also self-heals a missing root by +// re-creating directories on ENOENT during writes. +export const getHarnessCacheStores = ( + cacheRoot: string +): ((metroCache: MetroCache) => CacheStoresConfigT) => { + return ({ FileStore }) => [ + new FileStore({ root: cacheRoot }) as CacheStore< + TransformResult + >, + ]; }; diff --git a/packages/bundler-metro/src/paths.ts b/packages/bundler-metro/src/paths.ts index 7317da78..6ebbb4b5 100644 --- a/packages/bundler-metro/src/paths.ts +++ b/packages/bundler-metro/src/paths.ts @@ -1,32 +1,10 @@ -import fs from 'node:fs'; import path from 'node:path'; const HARNESS_DIRNAME = '.harness'; const MANIFEST_FILENAME = 'manifest.js'; -const METRO_CACHE_DIRNAME = 'metro-cache'; export const getHarnessRootPath = (projectRoot = process.cwd()): string => path.resolve(projectRoot, HARNESS_DIRNAME); export const getHarnessManifestPath = (projectRoot = process.cwd()): string => path.join(getHarnessRootPath(projectRoot), MANIFEST_FILENAME); - -export const getHarnessMetroCachePath = ( - projectRoot = process.cwd() -): string => path.join(getHarnessRootPath(projectRoot), METRO_CACHE_DIRNAME); - -export const isMetroCacheReusable = (projectRoot = process.cwd()): boolean => { - const metroCachePath = getHarnessMetroCachePath(projectRoot); - - try { - const stat = fs.statSync(metroCachePath); - - if (!stat.isDirectory()) { - return false; - } - - return fs.readdirSync(metroCachePath).length > 0; - } catch { - return false; - } -}; diff --git a/packages/bundler-metro/src/withRnHarness.ts b/packages/bundler-metro/src/withRnHarness.ts index 439814e2..b023e9be 100644 --- a/packages/bundler-metro/src/withRnHarness.ts +++ b/packages/bundler-metro/src/withRnHarness.ts @@ -1,9 +1,12 @@ import { createRequire } from 'node:module'; import os from 'node:os'; import type { MetroConfig } from 'metro-config'; +import { createHarnessCache } from '@react-native-harness/cache'; import { getConfig, + resolveMetroCacheEnabled, resolveSkipAlreadyIncludedModules, + type Config, } from '@react-native-harness/config'; import { logger } from '@react-native-harness/tools'; import { getHarnessBabelTransformerPath } from './babel-transformer.js'; @@ -20,6 +23,15 @@ const metroWorkersLogger = logger.child('metro-workers'); const INTERNAL_CALLSITES_REGEX = /(^|[\\/])(node_modules[/\\]@react-native-harness)([\\/]|$)/; +const getHarnessCacheVersion = (harnessConfig: Config): string => { + const { version } = require('../package.json') as { version: string }; + const userSalt = harnessConfig.cache?.version; + + return userSalt + ? `react-native-harness:${version}:${userSalt}` + : `react-native-harness:${version}`; +}; + export const withRnHarness = ( config: T | Promise, isInvokedByHarness = false, @@ -30,7 +42,10 @@ export const withRnHarness = ( } const metroConfig = await config; - const { config: harnessConfig } = await getConfig(process.cwd()); + const { config: harnessConfig, projectRoot } = await getConfig( + process.cwd() + ); + const harnessCache = createHarnessCache({ projectRoot }); const harnessResolver = getHarnessResolver(metroConfig, harnessConfig); const harnessManifest = getHarnessManifest(harnessConfig); @@ -54,7 +69,7 @@ export const withRnHarness = ( const patchedConfig: MetroConfig = { ...metroConfig, - cacheVersion: 'react-native-harness', + cacheVersion: getHarnessCacheVersion(harnessConfig), maxWorkers: cappedMaxWorkers, server: { ...metroConfig.server, @@ -84,6 +99,7 @@ export const withRnHarness = ( ...metroConfig.resolver, blockList: undefined, resolveRequest: harnessResolver, + useWatchman: process.env.RN_HARNESS_DEBUG_USE_WATCHMAN !== '0', }, transformer: { ...metroConfig.transformer, @@ -111,9 +127,25 @@ export const withRnHarness = ( }, }; - if (harnessConfig.unstable__enableMetroCache) { + if (resolveMetroCacheEnabled(harnessConfig)) { + harnessCache.ensureDomainDirectories('metro'); + (patchedConfig.cacheStores as NotReadOnly) = - getHarnessCacheStores(); + getHarnessCacheStores(harnessCache.paths.metroTransform); + + // Without an explicit directory the file-map cache lands in $TMPDIR; + // pin it under the harness cache root unless the user already picked a + // location. + if ( + metroConfig.fileMapCacheDirectory === undefined && + metroConfig.hasteMapCacheDirectory === undefined + ) { + ( + patchedConfig as NotReadOnly< + Pick + > + ).fileMapCacheDirectory = harnessCache.paths.metroFileMap; + } } if (resolveSkipAlreadyIncludedModules(harnessConfig)) { diff --git a/packages/bundler-metro/tsconfig.json b/packages/bundler-metro/tsconfig.json index 403a9dfe..5f3f6e8a 100644 --- a/packages/bundler-metro/tsconfig.json +++ b/packages/bundler-metro/tsconfig.json @@ -6,6 +6,9 @@ { "path": "../runtime" }, + { + "path": "../cache" + }, { "path": "../config" }, diff --git a/packages/bundler-metro/tsconfig.lib.json b/packages/bundler-metro/tsconfig.lib.json index 8f6398da..2eef96e4 100644 --- a/packages/bundler-metro/tsconfig.lib.json +++ b/packages/bundler-metro/tsconfig.lib.json @@ -15,6 +15,9 @@ { "path": "../runtime/tsconfig.lib.json" }, + { + "path": "../cache/tsconfig.lib.json" + }, { "path": "../config/tsconfig.lib.json" }, diff --git a/packages/cache/README.md b/packages/cache/README.md new file mode 100644 index 00000000..6c61e153 --- /dev/null +++ b/packages/cache/README.md @@ -0,0 +1,26 @@ +![harness-banner](https://react-native-harness.dev/harness-banner.jpg) + +### Cache Management for React Native Harness + +[![mit licence][license-badge]][license] +[![npm downloads][npm-downloads-badge]][npm-downloads] +[![Chat][chat-badge]][chat] +[![PRs Welcome][prs-welcome-badge]][prs-welcome] + +Owns the layout of `.harness/cache/` and exposes a narrow API for checking and preparing cache directories, so no other package needs to construct cache paths or hold cache-key logic. + +## Made with ❤️ at Callstack + +`@react-native-harness/cache` is an open source project and will always remain free to use. If you think it's cool, please star it 🌟. [Callstack][callstack-readme-with-love] is a group of React and React Native geeks, contact us at [hello@callstack.com](mailto:hello@callstack.com) if you need any help with these or just want to say hi! + +Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_campaign=Senior_RN&utm_source=github&utm_medium=readme) who does amazing stuff for clients and drives React Native Open Source! 🔥 + +[callstack-readme-with-love]: https://callstack.com/?utm_source=github.com&utm_medium=referral&utm_campaign=react-native-harness&utm_term=readme-with-love +[license-badge]: https://img.shields.io/npm/l/@react-native-harness/cache?style=for-the-badge +[license]: https://github.com/callstackincubator/react-native-harness/blob/main/LICENSE +[npm-downloads-badge]: https://img.shields.io/npm/dm/@react-native-harness/cache?style=for-the-badge +[npm-downloads]: https://www.npmjs.com/package/@react-native-harness/cache +[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge +[prs-welcome]: ../../CONTRIBUTING.md +[chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge +[chat]: https://discord.gg/xgGt7KAjxv diff --git a/packages/cache/eslint.config.mjs b/packages/cache/eslint.config.mjs new file mode 100644 index 00000000..97a5fc1f --- /dev/null +++ b/packages/cache/eslint.config.mjs @@ -0,0 +1,20 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], + ignoredDependencies: ['vite', 'vitest'], + }, + ], + }, + languageOptions: { + parser: await import('jsonc-eslint-parser'), + }, + }, +]; diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 00000000..63040334 --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,22 @@ +{ + "name": "@react-native-harness/cache", + "version": "1.3.0", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "dependencies": { + "@react-native-harness/tools": "workspace:*", + "tslib": "^2.3.0" + }, + "license": "MIT" +} diff --git a/packages/cache/src/__tests__/boundary.test.ts b/packages/cache/src/__tests__/boundary.test.ts new file mode 100644 index 00000000..e8779b54 --- /dev/null +++ b/packages/cache/src/__tests__/boundary.test.ts @@ -0,0 +1,95 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +// All `.harness/cache` layout knowledge must live in this package; other +// packages consume `HarnessCache.paths` instead of constructing cache paths +// themselves. This test scans the other packages' sources for violations. +// +// Allowlisted until phase 2 migrates them onto this package: +// - packages/tools/src/harness-artifacts.ts: getHarnessCacheRootPath, used by +// platform-ios for the XCTest agent cache. +const ALLOWLIST = new Set(['tools/src/harness-artifacts.ts']); + +const findRepoRoot = (start: string): string => { + let dir = start; + + while (!fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) { + const parent = path.dirname(dir); + if (parent === dir) { + throw new Error('Could not find the repository root'); + } + dir = parent; + } + + return dir; +}; + +const collectSourceFiles = (dir: string, files: string[] = []): string[] => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + if ( + entry.name === 'node_modules' || + entry.name === 'dist' || + entry.name === '__tests__' + ) { + continue; + } + collectSourceFiles(path.join(dir, entry.name), files); + } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) { + files.push(path.join(dir, entry.name)); + } + } + + return files; +}; + +const constructsHarnessCachePath = (content: string): boolean => { + if (content.includes('.harness/cache')) { + return true; + } + + const hasHarnessDirLiteral = + content.includes("'.harness'") || content.includes('".harness"'); + // A quoted 'cache' followed by `,`, `)`, or `]` is a path segment being + // joined, as opposed to e.g. a property name in a type. + const hasCacheSegmentLiteral = /['"]cache['"]\s*[,)\]]/.test(content); + + return hasHarnessDirLiteral && hasCacheSegmentLiteral; +}; + +describe('cache path boundary', () => { + it('no package outside @react-native-harness/cache constructs .harness cache paths', () => { + const repoRoot = findRepoRoot(path.dirname(fileURLToPath(import.meta.url))); + const packagesRoot = path.join(repoRoot, 'packages'); + + const violations: string[] = []; + + for (const entry of fs.readdirSync(packagesRoot, { + withFileTypes: true, + })) { + if (!entry.isDirectory() || entry.name === 'cache') { + continue; + } + + const srcDir = path.join(packagesRoot, entry.name, 'src'); + if (!fs.existsSync(srcDir)) { + continue; + } + + for (const file of collectSourceFiles(srcDir)) { + const relativePath = path.relative(packagesRoot, file); + if (ALLOWLIST.has(relativePath)) { + continue; + } + + if (constructsHarnessCachePath(fs.readFileSync(file, 'utf8'))) { + violations.push(relativePath); + } + } + } + + expect(violations).toEqual([]); + }); +}); diff --git a/packages/cache/src/__tests__/index.test.ts b/packages/cache/src/__tests__/index.test.ts new file mode 100644 index 00000000..166091a7 --- /dev/null +++ b/packages/cache/src/__tests__/index.test.ts @@ -0,0 +1,54 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createHarnessCache } from '../index.js'; + +describe('createHarnessCache', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('exposes paths rooted at /.harness/cache', () => { + const cache = createHarnessCache({ projectRoot }); + + expect(cache.paths.root).toBe(path.join(projectRoot, '.harness', 'cache')); + }); + + it('reports metro as cold before directories exist', () => { + const cache = createHarnessCache({ projectRoot }); + + expect(cache.isWarm('metro')).toBe(false); + }); + + it('creates domain directories and reports warm once populated', () => { + const cache = createHarnessCache({ projectRoot }); + + cache.ensureDomainDirectories('metro'); + + expect(fs.existsSync(cache.paths.metroTransform)).toBe(true); + expect(fs.existsSync(cache.paths.metroFileMap)).toBe(true); + expect(cache.isWarm('metro')).toBe(false); + + fs.writeFileSync(path.join(cache.paths.metroTransform, 'entry'), 'data'); + expect(cache.isWarm('metro')).toBe(true); + }); + + it('swallows and logs errors instead of throwing when directories cannot be created', () => { + // Point the project root at a path that cannot be created (its parent is a file). + const blockedRoot = path.join(projectRoot, 'blocked-file', 'nested'); + fs.writeFileSync(path.join(projectRoot, 'blocked-file'), 'not a directory'); + + const cache = createHarnessCache({ projectRoot: blockedRoot }); + + expect(() => cache.ensureDomainDirectories('metro')).not.toThrow(); + expect(cache.isWarm('metro')).toBe(false); + }); +}); diff --git a/packages/cache/src/__tests__/paths.test.ts b/packages/cache/src/__tests__/paths.test.ts new file mode 100644 index 00000000..bd60602b --- /dev/null +++ b/packages/cache/src/__tests__/paths.test.ts @@ -0,0 +1,18 @@ +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createHarnessCachePaths } from '../paths.js'; + +describe('createHarnessCachePaths', () => { + it('nests cache paths under /.harness/cache', () => { + const projectRoot = '/tmp/some-project'; + const paths = createHarnessCachePaths(projectRoot); + + expect(paths.root).toBe(path.join(projectRoot, '.harness', 'cache')); + expect(paths.metroTransform).toBe( + path.join(projectRoot, '.harness', 'cache', 'metro') + ); + expect(paths.metroFileMap).toBe( + path.join(projectRoot, '.harness', 'cache', 'metro-file-map') + ); + }); +}); diff --git a/packages/cache/src/__tests__/warmth.test.ts b/packages/cache/src/__tests__/warmth.test.ts new file mode 100644 index 00000000..80a5a4d9 --- /dev/null +++ b/packages/cache/src/__tests__/warmth.test.ts @@ -0,0 +1,47 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createHarnessCachePaths } from '../paths.js'; +import { isDomainWarm } from '../warmth.js'; + +describe('isDomainWarm', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + it('is false when the domain directory does not exist', () => { + const paths = createHarnessCachePaths(projectRoot); + + expect(isDomainWarm('metro', paths)).toBe(false); + }); + + it('is false when the domain directory exists but is empty', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + + expect(isDomainWarm('metro', paths)).toBe(false); + }); + + it('is true when the domain directory exists and has entries', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + fs.writeFileSync(path.join(paths.metroTransform, 'entry'), 'data'); + + expect(isDomainWarm('metro', paths)).toBe(true); + }); + + it('is false when the path exists but is a file, not a directory', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(path.dirname(paths.metroTransform), { recursive: true }); + fs.writeFileSync(paths.metroTransform, 'not a directory'); + + expect(isDomainWarm('metro', paths)).toBe(false); + }); +}); diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts new file mode 100644 index 00000000..e511d23f --- /dev/null +++ b/packages/cache/src/index.ts @@ -0,0 +1,42 @@ +import fs from 'node:fs'; +import { logger } from '@react-native-harness/tools'; +import { + createHarnessCachePaths, + getDomainDirectories, + type CacheDomainId, + type HarnessCachePaths, +} from './paths.js'; +import { isDomainWarm } from './warmth.js'; + +export type { CacheDomainId, HarnessCachePaths }; + +export interface HarnessCache { + readonly paths: HarnessCachePaths; + isWarm(domain: CacheDomainId): boolean; + ensureDomainDirectories(domain: CacheDomainId): void; +} + +const cacheLogger = logger.child('cache'); + +export const createHarnessCache = (options: { + projectRoot: string; +}): HarnessCache => { + const paths = createHarnessCachePaths(options.projectRoot); + + return { + paths, + isWarm: (domain) => isDomainWarm(domain, paths), + ensureDomainDirectories: (domain) => { + for (const directory of getDomainDirectories(domain, paths)) { + try { + fs.mkdirSync(directory, { recursive: true }); + } catch (error) { + cacheLogger.warn( + `Failed to create cache directory "${directory}". Continuing with a cold cache.`, + error + ); + } + } + }, + }; +}; diff --git a/packages/cache/src/paths.ts b/packages/cache/src/paths.ts new file mode 100644 index 00000000..e0efb102 --- /dev/null +++ b/packages/cache/src/paths.ts @@ -0,0 +1,43 @@ +import path from 'node:path'; + +const HARNESS_DIRNAME = '.harness'; +const CACHE_DIRNAME = 'cache'; +const METRO_TRANSFORM_DIRNAME = 'metro'; +const METRO_FILE_MAP_DIRNAME = 'metro-file-map'; + +export type CacheDomainId = 'metro'; + +export interface HarnessCachePaths { + readonly root: string; + readonly metroTransform: string; + readonly metroFileMap: string; +} + +export const createHarnessCachePaths = ( + projectRoot: string +): HarnessCachePaths => { + const root = path.join(projectRoot, HARNESS_DIRNAME, CACHE_DIRNAME); + + return { + root, + metroTransform: path.join(root, METRO_TRANSFORM_DIRNAME), + metroFileMap: path.join(root, METRO_FILE_MAP_DIRNAME), + }; +}; + +/** + * Directories that make up a cache domain. `metro` covers both the transform + * cache and the file-map (haste) cache so `ensureDomainDirectories` prepares + * both ahead of a run. + */ +export const getDomainDirectories = ( + domain: CacheDomainId, + paths: HarnessCachePaths +): readonly string[] => { + switch (domain) { + case 'metro': + return [paths.metroTransform, paths.metroFileMap]; + default: + return []; + } +}; diff --git a/packages/cache/src/warmth.ts b/packages/cache/src/warmth.ts new file mode 100644 index 00000000..c246e1a1 --- /dev/null +++ b/packages/cache/src/warmth.ts @@ -0,0 +1,59 @@ +import fs from 'node:fs'; +import { logger } from '@react-native-harness/tools'; +import type { CacheDomainId, HarnessCachePaths } from './paths.js'; + +const cacheLogger = logger.child('cache'); + +/** + * A domain is warm when its cache directory exists and is non-empty. Cache + * I/O must never fail a run, so any error while inspecting the filesystem is + * logged and treated as cold. + */ +export const isDomainWarm = ( + domain: CacheDomainId, + paths: HarnessCachePaths +): boolean => { + const directory = getWarmthDirectory(domain, paths); + + if (!directory) { + return false; + } + + try { + const stat = fs.statSync(directory); + + if (!stat.isDirectory()) { + return false; + } + + return fs.readdirSync(directory).length > 0; + } catch (error) { + if (isMissingPathError(error)) { + return false; + } + + cacheLogger.warn( + `Failed to check cache warmth for "${domain}" at "${directory}". Treating as cold.`, + error + ); + return false; + } +}; + +const getWarmthDirectory = ( + domain: CacheDomainId, + paths: HarnessCachePaths +): string | undefined => { + switch (domain) { + case 'metro': + return paths.metroTransform; + default: + return undefined; + } +}; + +const isMissingPathError = (error: unknown): boolean => + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code?: unknown }).code === 'ENOENT'; diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json new file mode 100644 index 00000000..a97d4269 --- /dev/null +++ b/packages/cache/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "../tools" + }, + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/cache/tsconfig.lib.json b/packages/cache/tsconfig.lib.json new file mode 100644 index 00000000..44a1da23 --- /dev/null +++ b/packages/cache/tsconfig.lib.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [ + { + "path": "../tools/tsconfig.lib.json" + } + ] +} diff --git a/packages/cache/vite.config.ts b/packages/cache/vite.config.ts new file mode 100644 index 00000000..9c4dedfc --- /dev/null +++ b/packages/cache/vite.config.ts @@ -0,0 +1,18 @@ +/// +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/cache', + test: { + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + }, + }, +})); diff --git a/packages/config/src/__tests__/resolveMetroCacheEnabled.test.ts b/packages/config/src/__tests__/resolveMetroCacheEnabled.test.ts new file mode 100644 index 00000000..140ff816 --- /dev/null +++ b/packages/config/src/__tests__/resolveMetroCacheEnabled.test.ts @@ -0,0 +1,126 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { ConfigSchema, resolveMetroCacheEnabled } from '../types.js'; + +const baseConfig = { + entryPoint: './index.js', + appRegistryComponentName: 'App', + runners: [ + { + name: 'test-runner', + config: {}, + runner: 'test-runner', + platformId: 'test-platform', + }, + ], +}; + +describe('ConfigSchema cache group', () => { + it('accepts a config without the cache group', () => { + const result = ConfigSchema.parse(baseConfig); + + expect(result.cache).toBeUndefined(); + expect(result.unstable__enableMetroCache).toBeUndefined(); + }); + + it('accepts cache.metro and cache.version', () => { + const result = ConfigSchema.parse({ + ...baseConfig, + cache: { metro: false, version: 'v2' }, + }); + + expect(result.cache).toEqual({ metro: false, version: 'v2' }); + }); + + it('passes cache.version through untouched when metro is unset', () => { + const result = ConfigSchema.parse({ + ...baseConfig, + cache: { version: 'salt-1' }, + }); + + expect(result.cache?.metro).toBeUndefined(); + expect(result.cache?.version).toBe('salt-1'); + }); + + it('rejects a non-string cache.version', () => { + expect(() => + ConfigSchema.parse({ ...baseConfig, cache: { version: 2 } }) + ).toThrow(); + }); +}); + +describe('resolveMetroCacheEnabled', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('defaults to true when neither cache.metro nor the alias is set', () => { + expect( + resolveMetroCacheEnabled({ + cache: undefined, + unstable__enableMetroCache: undefined, + }) + ).toBe(true); + + expect( + resolveMetroCacheEnabled({ + cache: {}, + unstable__enableMetroCache: undefined, + }) + ).toBe(true); + }); + + it('uses the deprecated alias when only it is set', () => { + expect( + resolveMetroCacheEnabled({ + cache: undefined, + unstable__enableMetroCache: false, + }) + ).toBe(false); + + expect( + resolveMetroCacheEnabled({ + cache: undefined, + unstable__enableMetroCache: true, + }) + ).toBe(true); + }); + + it('logs a deprecation warning when the alias is set', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + resolveMetroCacheEnabled({ + cache: undefined, + unstable__enableMetroCache: true, + }); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy.mock.calls[0]?.[0]).toContain('unstable__enableMetroCache'); + }); + + it('does not log a deprecation warning when only cache.metro is set', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + resolveMetroCacheEnabled({ + cache: { metro: false }, + unstable__enableMetroCache: undefined, + }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('lets explicit cache.metro win over the alias in either direction', () => { + expect( + resolveMetroCacheEnabled({ + cache: { metro: false }, + unstable__enableMetroCache: true, + }) + ).toBe(false); + + expect( + resolveMetroCacheEnabled({ + cache: { metro: true }, + unstable__enableMetroCache: false, + }) + ).toBe(true); + }); +}); diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index edaffdd0..522bb8fc 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -4,6 +4,7 @@ export { ConfigSchema, DEFAULT_METRO_PORT, isDiagnosticsEnabled, + resolveMetroCacheEnabled, resolveSkipAlreadyIncludedModules, } from './types.js'; export { diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index f0bb43d3..3a4eb1d0 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -114,7 +114,36 @@ export const ConfigSchema = z .describe( 'Deprecated. Use `skipAlreadyIncludedModules` instead.' ), - unstable__enableMetroCache: z.boolean().optional().default(false), + cache: z + .object({ + metro: z + .boolean() + .optional() + .describe( + 'Enable persistent Metro transform and file-map caching under the .harness cache directory. ' + + 'Defaults to true; set to false to always start with a cold Metro cache. ' + + 'Left undefined (rather than defaulted here) so `resolveMetroCacheEnabled` can tell ' + + '"not set" apart from "explicitly set" when reconciling with the deprecated ' + + '`unstable__enableMetroCache` alias.' + ), + version: z + .string() + .optional() + .describe( + 'User-controlled salt folded into the Metro cacheVersion. ' + + 'Bump it to force-invalidate previously cached transforms.' + ), + }) + .optional(), + // Deprecated alias for `cache.metro` -- see `resolveMetroCacheEnabled`. + // Left without a `.default()` for the same reason as + // `unstable__skipAlreadyIncludedModules`: a default value would be + // indistinguishable from the user never having set it, which would break + // alias-vs-explicit-flag precedence. + unstable__enableMetroCache: z + .boolean() + .optional() + .describe('Deprecated. Use `cache.metro` instead.'), permissions: z .boolean() .optional() @@ -263,3 +292,38 @@ export const resolveSkipAlreadyIncludedModules = ( return true; }; + +/** + * Resolves whether persistent Metro caching is enabled, reconciling + * `cache.metro` with the deprecated `unstable__enableMetroCache` alias: + * + * - `cache.metro`, when explicitly set, always wins over the alias. + * - Otherwise, the alias's value is used (with a deprecation warning). + * - Otherwise, defaults to `true`. + * + * The default is applied here rather than in the zod schema so that "unset" + * and "explicitly set to the default value" remain distinguishable -- that + * distinction is what lets the deprecated alias keep working as an escape + * hatch (e.g. `unstable__enableMetroCache: false`) even though caching now + * defaults to on. + */ +export const resolveMetroCacheEnabled = ( + config: Pick +): boolean => { + if (config.unstable__enableMetroCache !== undefined) { + configLogger.warn( + '`unstable__enableMetroCache` is deprecated and will be removed in a future release. ' + + 'Use `cache.metro` instead.' + ); + } + + if (config.cache?.metro !== undefined) { + return config.cache.metro; + } + + if (config.unstable__enableMetroCache !== undefined) { + return config.unstable__enableMetroCache; + } + + return true; +}; diff --git a/packages/github-action/README.md b/packages/github-action/README.md index 30619425..b10b9f14 100644 --- a/packages/github-action/README.md +++ b/packages/github-action/README.md @@ -33,7 +33,7 @@ The action reads your `rn-harness.config.mjs` file, resolves the `runner` you pa - `preRunHook` (optional): Inline shell script run in `bash` immediately before Harness starts - `afterRunHook` (optional): Inline shell script run in `bash` immediately after Harness finishes and before artifact upload - Crash artifacts persisted to `.harness/crash-reports/` are uploaded automatically when present -- Metro cache persisted to `.harness/metro-cache/` is restored and saved automatically when present +- Metro cache persisted to `.harness/cache/metro/` and `.harness/cache/metro-file-map/` is restored and saved automatically when present ## Behavior diff --git a/packages/github-action/src/action.yml b/packages/github-action/src/action.yml index f9098959..09cf8899 100644 --- a/packages/github-action/src/action.yml +++ b/packages/github-action/src/action.yml @@ -64,13 +64,15 @@ runs: echo "Please provide the path to the built app (.apk for Android, .app for iOS)" exit 1 fi - - name: Metro bundler cache (.harness/metro-cache) + - name: Metro bundler cache (.harness/cache/metro) uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ steps.load-config.outputs.projectRoot }}/.harness/metro-cache - key: ${{ runner.os }}-metro-cache-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }} + path: | + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro-file-map + key: ${{ runner.os }}-metro-cache-v2-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }} restore-keys: | - ${{ runner.os }}-metro-cache- + ${{ runner.os }}-metro-cache-v2- - name: Restore Harness cache id: cache-harness-restore if: fromJson(steps.load-config.outputs.config).platformId == 'ios' diff --git a/packages/jest/package.json b/packages/jest/package.json index e9958fbd..305ae20b 100644 --- a/packages/jest/package.json +++ b/packages/jest/package.json @@ -36,6 +36,7 @@ "@jest/test-result": "^30.2.0", "@react-native-harness/bridge": "workspace:*", "@react-native-harness/bundler-metro": "workspace:*", + "@react-native-harness/cache": "workspace:*", "@react-native-harness/config": "workspace:*", "@react-native-harness/plugins": "workspace:*", "@react-native-harness/platforms": "workspace:*", diff --git a/packages/jest/src/__tests__/harness-cache.test.ts b/packages/jest/src/__tests__/harness-cache.test.ts new file mode 100644 index 00000000..3772a1a1 --- /dev/null +++ b/packages/jest/src/__tests__/harness-cache.test.ts @@ -0,0 +1,91 @@ +import type { HarnessPlatform } from '@react-native-harness/platforms'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { maybeLogMetroCacheReused } from '../harness-session.js'; + +const { createHarnessCache, isWarm, logMetroCacheReused } = vi.hoisted(() => { + const isWarm = vi.fn(); + + return { + isWarm, + createHarnessCache: vi.fn((options: { projectRoot: string }) => ({ + paths: { + root: `${options.projectRoot}/.harness/cache`, + metroTransform: `${options.projectRoot}/.harness/cache/metro`, + metroFileMap: `${options.projectRoot}/.harness/cache/metro-file-map`, + }, + isWarm, + ensureDomainDirectories: vi.fn(), + })), + logMetroCacheReused: vi.fn(), + }; +}); + +vi.mock('@react-native-harness/cache', () => ({ + createHarnessCache, +})); + +vi.mock('../logs.js', async (importOriginal) => ({ + ...(await importOriginal()), + logMetroCacheReused, +})); + +const platform = { name: 'test-runner' } as HarnessPlatform; + +describe('maybeLogMetroCacheReused', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('logs the reuse message when caching is enabled and the cache is warm', () => { + isWarm.mockReturnValue(true); + + maybeLogMetroCacheReused({ + config: { cache: { metro: true } }, + projectRoot: '/tmp/app', + platform, + }); + + expect(createHarnessCache).toHaveBeenCalledWith({ + projectRoot: '/tmp/app', + }); + expect(isWarm).toHaveBeenCalledWith('metro'); + expect(logMetroCacheReused).toHaveBeenCalledWith(platform); + }); + + it('logs the reuse message with a warm cache when caching is on by default', () => { + isWarm.mockReturnValue(true); + + maybeLogMetroCacheReused({ + config: {}, + projectRoot: '/tmp/app', + platform, + }); + + expect(logMetroCacheReused).toHaveBeenCalledWith(platform); + }); + + it('does not log when caching is enabled but the cache is cold', () => { + isWarm.mockReturnValue(false); + + maybeLogMetroCacheReused({ + config: { cache: { metro: true } }, + projectRoot: '/tmp/app', + platform, + }); + + expect(logMetroCacheReused).not.toHaveBeenCalled(); + }); + + it('does not log when caching is disabled, regardless of cache state', () => { + isWarm.mockReturnValue(true); + + maybeLogMetroCacheReused({ + config: { cache: { metro: false } }, + projectRoot: '/tmp/app', + platform, + }); + + expect(createHarnessCache).not.toHaveBeenCalled(); + expect(logMetroCacheReused).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/jest/src/harness-session.ts b/packages/jest/src/harness-session.ts index f580aeee..083a4e4d 100644 --- a/packages/jest/src/harness-session.ts +++ b/packages/jest/src/harness-session.ts @@ -19,9 +19,9 @@ import { type HarnessPlatformInitOptions, type HarnessPlatformRunner, } from '@react-native-harness/platforms'; +import { createHarnessCache } from '@react-native-harness/cache'; import { getMetroInstance, - isMetroCacheReusable, waitForMetroBackedAppReady, type MetroInstance, type MetroWebSocketEndpoint, @@ -46,6 +46,7 @@ import { import { getConfig, isDiagnosticsEnabled, + resolveMetroCacheEnabled, type Config as HarnessConfig, ConfigSchema, } from '@react-native-harness/config'; @@ -399,13 +400,37 @@ const applyEnvVars = ( } }; +/** + * Logs the "Reusing Metro cache" message when persistent Metro caching is + * enabled and the cache is warm. `projectRoot` must be the config-resolved + * project root so the warmth check looks at the same directory Metro caches + * into. + */ +export const maybeLogMetroCacheReused = (options: { + config: Pick; + projectRoot: string; + platform: HarnessPlatform; +}): void => { + if (!resolveMetroCacheEnabled(options.config)) { + return; + } + + if ( + createHarnessCache({ projectRoot: options.projectRoot }).isWarm('metro') + ) { + logMetroCacheReused(options.platform); + } +}; + const loadConfig = async (globalConfig: JestConfig.GlobalConfig): Promise<{ harnessConfig: HarnessConfig; platform: HarnessPlatform; projectRoot: string; + configProjectRoot: string; }> => { const projectRoot = globalConfig.rootDir; - const { config: rawConfig } = await getConfig(projectRoot); + const { config: rawConfig, projectRoot: configProjectRoot } = + await getConfig(projectRoot); const cliArgs = getAdditionalCliArgs(); let harnessConfig = cliArgs.metroPort != null @@ -434,7 +459,7 @@ const loadConfig = async (globalConfig: JestConfig.GlobalConfig): Promise<{ const platform = harnessConfig.runners.find((r) => r.name === selectedRunnerName); if (!platform) throw new RunnerNotFoundError(selectedRunnerName); - return { harnessConfig, platform, projectRoot }; + return { harnessConfig, platform, projectRoot, configProjectRoot }; }; // --------------------------------------------------------------------------- @@ -448,7 +473,7 @@ export const createHarnessSession = async ( preRunMessage.remove(process.stderr); const setupStartedAt = Date.now(); - const { harnessConfig, platform, projectRoot } = await loadConfig(globalConfig); + const { harnessConfig, platform, projectRoot, configProjectRoot } = await loadConfig(globalConfig); applyEnvVars(harnessConfig, globalConfig); const diagnostics = createDiagnostics({ @@ -529,9 +554,11 @@ export const createHarnessSession = async ( if (didFallback) logMetroPortFallback(initialMetroPort, runtimeConfig.metroPort); - if (runtimeConfig.unstable__enableMetroCache && isMetroCacheReusable(projectRoot)) { - logMetroCacheReused(platform); - } + maybeLogMetroCacheReused({ + config: runtimeConfig, + projectRoot: configProjectRoot, + platform, + }); const pluginAbortController = new AbortController(); const pluginManager = createHarnessPluginManager({ diff --git a/packages/jest/tsconfig.json b/packages/jest/tsconfig.json index d186efc1..c04ed7e2 100644 --- a/packages/jest/tsconfig.json +++ b/packages/jest/tsconfig.json @@ -6,6 +6,9 @@ { "path": "../tools" }, + { + "path": "../cache" + }, { "path": "../platforms" }, diff --git a/packages/jest/tsconfig.lib.json b/packages/jest/tsconfig.lib.json index 6e003874..f468351c 100644 --- a/packages/jest/tsconfig.lib.json +++ b/packages/jest/tsconfig.lib.json @@ -15,6 +15,9 @@ { "path": "../tools/tsconfig.lib.json" }, + { + "path": "../cache/tsconfig.lib.json" + }, { "path": "../platforms/tsconfig.lib.json" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cacf6e37..239dda3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -263,6 +263,9 @@ importers: '@react-native-harness/babel-preset': specifier: workspace:* version: link:../babel-preset + '@react-native-harness/cache': + specifier: workspace:* + version: link:../cache '@react-native-harness/config': specifier: workspace:* version: link:../config @@ -301,6 +304,15 @@ importers: specifier: '*' version: 0.83.3 + packages/cache: + dependencies: + '@react-native-harness/tools': + specifier: workspace:* + version: link:../tools + tslib: + specifier: ^2.3.0 + version: 2.8.1 + packages/cli: dependencies: '@react-native-harness/bridge': @@ -381,6 +393,9 @@ importers: '@react-native-harness/bundler-metro': specifier: workspace:* version: link:../bundler-metro + '@react-native-harness/cache': + specifier: workspace:* + version: link:../cache '@react-native-harness/config': specifier: workspace:* version: link:../config diff --git a/tsconfig.json b/tsconfig.json index 17c62baa..acde8899 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,9 @@ { "path": "./packages/tools" }, + { + "path": "./packages/cache" + }, { "path": "./packages/react-native-harness" }, diff --git a/website/src/docs/getting-started/configuration.mdx b/website/src/docs/getting-started/configuration.mdx index 5203e7c9..72449980 100644 --- a/website/src/docs/getting-started/configuration.mdx +++ b/website/src/docs/getting-started/configuration.mdx @@ -111,7 +111,10 @@ For Expo projects, the `entryPoint` should be set to the path specified in the ` | `coverage.root` | Root directory for coverage instrumentation (default: `process.cwd()`). | | `coverage.native.ios.pods` | Experimental list of CocoaPods target names to instrument for iOS native coverage. | | `forwardClientLogs` | Forward console logs from the app to the terminal (default: `false`). | -| `unstable__enableMetroCache` | Enable Metro transformation cache under `.harness/metro-cache` and log when reusing it (default: `false`). | +| `cache` | Cache configuration object. | +| `cache.metro` | Enable persistent Metro transform and file-map caching under `.harness/cache` (default: `true`). | +| `cache.version` | User-controlled salt folded into the Metro cache version. Bump it to force-invalidate previously cached transforms. | +| `unstable__enableMetroCache` | Deprecated. Use `cache.metro` instead. | ## Test Runners diff --git a/website/src/docs/guides/ci-cd.md b/website/src/docs/guides/ci-cd.md index afa4c9c7..ae17381f 100644 --- a/website/src/docs/guides/ci-cd.md +++ b/website/src/docs/guides/ci-cd.md @@ -247,9 +247,9 @@ If your workflow does not define AVD details, the action can still run the tests ## Metro cache -React Native Harness can persist Metro's transformation cache under `.harness/metro-cache` in your project root. Enabling it in config (`unstable__enableMetroCache: true`) speeds up repeated Metro runs. +React Native Harness persists Metro's transform cache under `.harness/cache/metro` and its file-map (haste) cache under `.harness/cache/metro-file-map`, both in your project root. This is on by default (`cache.metro: true`); set `cache.metro: false` in config to disable it. The deprecated `unstable__enableMetroCache` option is still honored as an alias for one release. -When you use the `callstackincubator/react-native-harness` GitHub Action, Metro cache restoration and saving is handled automatically for the resolved `projectRoot`. You do not need to add a separate `actions/cache` step for `.harness/metro-cache`. +When you use the `callstackincubator/react-native-harness` GitHub Action, Metro cache restoration and saving is handled automatically for the resolved `projectRoot`. You do not need to add a separate `actions/cache` step for `.harness/cache/metro` or `.harness/cache/metro-file-map`. ## Web in CI From ca3cb197a86736002ee1d25fdd372c509a3ae980 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 12:16:54 +0200 Subject: [PATCH 2/9] feat: compute Metro cache keys from resolved inputs, fix iOS cache overlap Replace the static hashFiles()-based Metro cache key with keys computed by Harness itself: lockfiles and Metro/Babel configs (walked from the repo root, excluding node_modules, fixing a latent overmatch in the old hashFiles glob), the resolved @react-native-harness/bundler-metro version, and the cache.version salt. A pre-run "plan restore" step derives the restore key/prefixes and writes .harness/cache/keys.json; a post-run "plan save" step diffs the actual cache contents against what was restored and only saves a new immutable entry when content changed, gated by a new cacheSavePolicy action input (default-branch/always/never). Also narrows the iOS "Harness cache" step's cached path from the whole .harness/cache root to its actual xctest-agent-simulator-* subdirectory, fixing a double-upload of the Metro cache on iOS runs introduced when both caches started sharing .harness/cache. --- .nx/version-plans/compute-metro-cache-keys.md | 5 + action.yml | 58 +- actions/shared/plan-restore.cjs | 4981 ++++++++++++++++ actions/shared/plan-save.cjs | 5004 +++++++++++++++++ actions/shared/snapshot-metro.cjs | 4881 ++++++++++++++++ .../cache/src/__tests__/domains/metro.test.ts | 156 + packages/cache/src/__tests__/plan.test.ts | 330 ++ packages/cache/src/domains/metro.ts | 103 + packages/cache/src/index.ts | 42 + packages/cache/src/plan.ts | 285 + packages/github-action/README.md | 3 +- packages/github-action/eslint.config.mjs | 8 + packages/github-action/package.json | 1 + packages/github-action/src/action.yml | 58 +- .../__tests__/metro-cache-inputs.test.ts | 77 + .../src/shared/__tests__/plan-restore.test.ts | 123 + .../src/shared/__tests__/plan-save.test.ts | 152 + .../shared/__tests__/snapshot-metro.test.ts | 82 + .../src/shared/metro-cache-inputs.ts | 75 + .../github-action/src/shared/plan-restore.ts | 69 + .../github-action/src/shared/plan-save.ts | 96 + .../src/shared/snapshot-metro.ts | 49 + packages/github-action/tsconfig.json | 8 +- packages/github-action/tsconfig.lib.json | 8 +- packages/github-action/tsup.config.mts | 3 + packages/github-action/vite.config.ts | 18 + pnpm-lock.yaml | 3 + website/src/docs/guides/ci-cd.md | 13 +- 28 files changed, 16673 insertions(+), 18 deletions(-) create mode 100644 .nx/version-plans/compute-metro-cache-keys.md create mode 100644 actions/shared/plan-restore.cjs create mode 100644 actions/shared/plan-save.cjs create mode 100644 actions/shared/snapshot-metro.cjs create mode 100644 packages/cache/src/__tests__/domains/metro.test.ts create mode 100644 packages/cache/src/__tests__/plan.test.ts create mode 100644 packages/cache/src/domains/metro.ts create mode 100644 packages/cache/src/plan.ts create mode 100644 packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts create mode 100644 packages/github-action/src/shared/__tests__/plan-restore.test.ts create mode 100644 packages/github-action/src/shared/__tests__/plan-save.test.ts create mode 100644 packages/github-action/src/shared/__tests__/snapshot-metro.test.ts create mode 100644 packages/github-action/src/shared/metro-cache-inputs.ts create mode 100644 packages/github-action/src/shared/plan-restore.ts create mode 100644 packages/github-action/src/shared/plan-save.ts create mode 100644 packages/github-action/src/shared/snapshot-metro.ts create mode 100644 packages/github-action/vite.config.ts diff --git a/.nx/version-plans/compute-metro-cache-keys.md b/.nx/version-plans/compute-metro-cache-keys.md new file mode 100644 index 00000000..9a8fa590 --- /dev/null +++ b/.nx/version-plans/compute-metro-cache-keys.md @@ -0,0 +1,5 @@ +--- +__default__: minor +--- + +The official GitHub Action now computes the Metro cache key itself instead of hashing a static file list: it accounts for your lockfile(s), Metro/Babel config, the resolved `@react-native-harness/bundler-metro` version, and your `cache.version` salt, so the cache invalidates automatically when you upgrade Metro, not only when a lockfile or config file changes. A new `cacheSavePolicy` action input (`'default-branch'` by default, or `'always'`/`'never'`) controls when a new cache entry is saved, and a run only saves a new entry when its cache contents actually changed. diff --git a/action.yml b/action.yml index 09cf8899..655608b7 100644 --- a/action.yml +++ b/action.yml @@ -43,6 +43,11 @@ inputs: required: false type: string default: '' + cacheSavePolicy: + description: When to save a new Metro cache entry ('default-branch', 'always', or 'never') + required: false + type: string + default: 'default-branch' runs: using: 'composite' steps: @@ -64,21 +69,41 @@ runs: echo "Please provide the path to the built app (.apk for Android, .app for iOS)" exit 1 fi - - name: Metro bundler cache (.harness/cache/metro) - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + - name: Plan Metro cache restore + id: plan-metro-restore + shell: bash + env: + INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + run: | + node ${{ github.action_path }}/actions/shared/plan-restore.cjs + - name: Restore Metro cache (.harness/cache/metro) + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro-file-map - key: ${{ runner.os }}-metro-cache-v2-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }} - restore-keys: | - ${{ runner.os }}-metro-cache-v2- + key: ${{ steps.plan-metro-restore.outputs.metroRestoreKey }} + # The '\n' here is a literal newline embedded via YAML double-quote + # escaping (not a GitHub Actions expression escape -- expression + # string literals don't interpret backslash sequences), so join() + # actually separates entries onto their own lines, as actions/cache + # requires for restore-keys. + restore-keys: "${{ join(fromJson(steps.plan-metro-restore.outputs.metroRestorePrefixes), '\n') }}" + # Must run after "Restore Metro cache" so this reflects what was actually + # restored (empty on a cache miss), not the always-empty pre-restore state. + - name: Snapshot Metro cache + id: snapshot-metro-restore + shell: bash + env: + INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + run: | + node ${{ github.action_path }}/actions/shared/snapshot-metro.cjs - name: Restore Harness cache id: cache-harness-restore if: fromJson(steps.load-config.outputs.config).platformId == 'ios' uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache + path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/xctest-agent-simulator-* key: harness-ios-${{ runner.os }}-${{ hashFiles(format('{0}/.harness/cache/**/cache.json', steps.load-config.outputs.projectRoot)) }} restore-keys: | harness-ios-${{ runner.os }}- @@ -242,8 +267,27 @@ runs: if: ${{ always() && fromJson(steps.load-config.outputs.config).platformId == 'ios' && steps.cache-harness-restore.outputs.cache-hit != 'true' }} uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache + path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/xctest-agent-simulator-* key: ${{ steps.cache-harness-restore.outputs.cache-primary-key }} + - name: Plan Metro cache save + id: plan-metro-save + if: always() + shell: bash + env: + INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + INPUT_METRO_SNAPSHOT: ${{ steps.snapshot-metro-restore.outputs.metroSnapshot }} + INPUT_CACHESAVEPOLICY: ${{ inputs.cacheSavePolicy }} + IS_DEFAULT_BRANCH: ${{ github.ref_name == github.event.repository.default_branch }} + run: | + node ${{ github.action_path }}/actions/shared/plan-save.cjs + - name: Save Metro cache + if: always() && steps.plan-metro-save.outputs.metroShouldSave == 'true' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro-file-map + key: ${{ steps.plan-metro-save.outputs.metroSaveKey }} - name: Upload crash report artifacts if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/actions/shared/plan-restore.cjs b/actions/shared/plan-restore.cjs new file mode 100644 index 00000000..523b0c55 --- /dev/null +++ b/actions/shared/plan-restore.cjs @@ -0,0 +1,4981 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// ../../node_modules/picocolors/picocolors.js +var require_picocolors = __commonJS({ + "../../node_modules/picocolors/picocolors.js"(exports2, module2) { + "use strict"; + var p = process || {}; + var argv = p.argv || []; + var env = p.env || {}; + var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI); + var formatter = (open, close, replace = open) => (input) => { + let string = "" + input, index = string.indexOf(close, open.length); + return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; + }; + var replaceClose = (string, close, replace, index) => { + let result = "", cursor = 0; + do { + result += string.substring(cursor, index) + replace; + cursor = index + close.length; + index = string.indexOf(close, cursor); + } while (~index); + return result + string.substring(cursor); + }; + var createColors = (enabled = isColorSupported) => { + let f = enabled ? formatter : () => String; + return { + isColorSupported: enabled, + reset: f("\x1B[0m", "\x1B[0m"), + bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"), + dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"), + italic: f("\x1B[3m", "\x1B[23m"), + underline: f("\x1B[4m", "\x1B[24m"), + inverse: f("\x1B[7m", "\x1B[27m"), + hidden: f("\x1B[8m", "\x1B[28m"), + strikethrough: f("\x1B[9m", "\x1B[29m"), + black: f("\x1B[30m", "\x1B[39m"), + red: f("\x1B[31m", "\x1B[39m"), + green: f("\x1B[32m", "\x1B[39m"), + yellow: f("\x1B[33m", "\x1B[39m"), + blue: f("\x1B[34m", "\x1B[39m"), + magenta: f("\x1B[35m", "\x1B[39m"), + cyan: f("\x1B[36m", "\x1B[39m"), + white: f("\x1B[37m", "\x1B[39m"), + gray: f("\x1B[90m", "\x1B[39m"), + bgBlack: f("\x1B[40m", "\x1B[49m"), + bgRed: f("\x1B[41m", "\x1B[49m"), + bgGreen: f("\x1B[42m", "\x1B[49m"), + bgYellow: f("\x1B[43m", "\x1B[49m"), + bgBlue: f("\x1B[44m", "\x1B[49m"), + bgMagenta: f("\x1B[45m", "\x1B[49m"), + bgCyan: f("\x1B[46m", "\x1B[49m"), + bgWhite: f("\x1B[47m", "\x1B[49m"), + blackBright: f("\x1B[90m", "\x1B[39m"), + redBright: f("\x1B[91m", "\x1B[39m"), + greenBright: f("\x1B[92m", "\x1B[39m"), + yellowBright: f("\x1B[93m", "\x1B[39m"), + blueBright: f("\x1B[94m", "\x1B[39m"), + magentaBright: f("\x1B[95m", "\x1B[39m"), + cyanBright: f("\x1B[96m", "\x1B[39m"), + whiteBright: f("\x1B[97m", "\x1B[39m"), + bgBlackBright: f("\x1B[100m", "\x1B[49m"), + bgRedBright: f("\x1B[101m", "\x1B[49m"), + bgGreenBright: f("\x1B[102m", "\x1B[49m"), + bgYellowBright: f("\x1B[103m", "\x1B[49m"), + bgBlueBright: f("\x1B[104m", "\x1B[49m"), + bgMagentaBright: f("\x1B[105m", "\x1B[49m"), + bgCyanBright: f("\x1B[106m", "\x1B[49m"), + bgWhiteBright: f("\x1B[107m", "\x1B[49m") + }; + }; + module2.exports = createColors(); + module2.exports.createColors = createColors; + } +}); + +// ../../node_modules/sisteransi/src/index.js +var require_src = __commonJS({ + "../../node_modules/sisteransi/src/index.js"(exports2, module2) { + "use strict"; + var ESC = "\x1B"; + var CSI = `${ESC}[`; + var beep = "\x07"; + var cursor = { + to(x2, y) { + if (!y) return `${CSI}${x2 + 1}G`; + return `${CSI}${y + 1};${x2 + 1}H`; + }, + move(x2, y) { + let ret = ""; + if (x2 < 0) ret += `${CSI}${-x2}D`; + else if (x2 > 0) ret += `${CSI}${x2}C`; + if (y < 0) ret += `${CSI}${-y}A`; + else if (y > 0) ret += `${CSI}${y}B`; + return ret; + }, + up: (count = 1) => `${CSI}${count}A`, + down: (count = 1) => `${CSI}${count}B`, + forward: (count = 1) => `${CSI}${count}C`, + backward: (count = 1) => `${CSI}${count}D`, + nextLine: (count = 1) => `${CSI}E`.repeat(count), + prevLine: (count = 1) => `${CSI}F`.repeat(count), + left: `${CSI}G`, + hide: `${CSI}?25l`, + show: `${CSI}?25h`, + save: `${ESC}7`, + restore: `${ESC}8` + }; + var scroll = { + up: (count = 1) => `${CSI}S`.repeat(count), + down: (count = 1) => `${CSI}T`.repeat(count) + }; + var erase = { + screen: `${CSI}2J`, + up: (count = 1) => `${CSI}1J`.repeat(count), + down: (count = 1) => `${CSI}J`.repeat(count), + line: `${CSI}2K`, + lineEnd: `${CSI}K`, + lineStart: `${CSI}1K`, + lines(count) { + let clear = ""; + for (let i = 0; i < count; i++) + clear += this.line + (i < count - 1 ? cursor.up() : ""); + if (count) + clear += cursor.left; + return clear; + } + }; + module2.exports = { cursor, scroll, erase, beep }; + } +}); + +// src/shared/plan-restore.ts +var import_node_fs12 = __toESM(require("fs")); +var import_node_path12 = __toESM(require("path")); + +// ../cache/dist/index.js +var import_node_fs9 = __toESM(require("fs"), 1); + +// ../tools/dist/net.js +var import_node_net = __toESM(require("net"), 1); + +// ../tools/dist/logger.js +var import_node_util = __toESM(require("util"), 1); +var import_picocolors = __toESM(require_picocolors(), 1); +var verbose = !!process.env.HARNESS_DEBUG; +var BASE_TAG = "[harness]"; +var INFO_TAG = import_picocolors.default.isColorSupported ? import_picocolors.default.reset(import_picocolors.default.inverse(import_picocolors.default.bold(import_picocolors.default.magenta(" HARNESS ")))) : "HARNESS"; +var ERROR_TAG = import_picocolors.default.isColorSupported ? import_picocolors.default.reset(import_picocolors.default.inverse(import_picocolors.default.bold(import_picocolors.default.red(" HARNESS ")))) : "HARNESS"; +var getTimestamp = () => (/* @__PURE__ */ new Date()).toISOString(); +var normalizeScope = (scope) => scope.trim().replace(/^\[+|\]+$/g, "").replace(/\]\[/g, "]["); +var formatPrefix = (scopes) => { + const suffix = scopes.map((scope) => `[${normalizeScope(scope)}]`).join(""); + return `${BASE_TAG}${suffix}`; +}; +var mapLines = (text, prefix) => text.split("\n").map((line) => `${prefix} ${line}`).join("\n"); +var writeLog = (level, scopes, messages) => { + if (!verbose && (level === "info" || level === "log" || level === "success")) { + const output2 = import_node_util.default.format(...messages); + const tag = INFO_TAG; + process.stderr.write(`${tag} ${output2} +`); + return; + } + if (!verbose && level === "error") { + const output2 = import_node_util.default.format(...messages); + process.stderr.write(`${ERROR_TAG} ${output2} +`); + return; + } + const method = level === "warn" ? console.warn : console.debug; + const output = import_node_util.default.format(...messages); + const prefix = `${getTimestamp()} ${formatPrefix(scopes)}`; + method(mapLines(output, prefix)); +}; +var setVerbose = (level) => { + verbose = level; +}; +var isVerbose = () => { + return verbose; +}; +var createScopedLogger = (scopes = []) => ({ + debug: (...messages) => { + if (!verbose) { + return; + } + writeLog("debug", scopes, messages); + }, + info: (...messages) => { + writeLog("info", scopes, messages); + }, + warn: (...messages) => { + writeLog("warn", scopes, messages); + }, + error: (...messages) => { + writeLog("error", scopes, messages); + }, + log: (...messages) => { + writeLog("log", scopes, messages); + }, + success: (...messages) => { + writeLog("success", scopes, messages); + }, + child: (scope) => createScopedLogger([...scopes, scope]), + setVerbose, + isVerbose +}); +var logger = createScopedLogger(); + +// ../../node_modules/@clack/core/dist/index.mjs +var import_node_process = require("process"); +var k = __toESM(require("readline"), 1); +var import_node_readline = __toESM(require("readline"), 1); +var import_sisteransi = __toESM(require_src(), 1); +var import_node_tty = require("tty"); +var Ft = { limit: 1 / 0, ellipsis: "" }; +var ft = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var j = "\x07"; +var Q = "["; +var dt = "]"; +var U = `${dt}8;;`; +var et = new RegExp(`(?:\\${Q}(?\\d+)m|\\${U}(?.*)${j})`, "y"); +var At = ["up", "down", "left", "right", "space", "enter", "cancel"]; +var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true }; +var bt = globalThis.process.platform.startsWith("win"); + +// ../../node_modules/@clack/prompts/dist/index.mjs +var import_picocolors2 = __toESM(require_picocolors(), 1); +var import_node_process2 = __toESM(require("process"), 1); +var import_node_fs = require("fs"); +var import_node_path = require("path"); +var import_sisteransi2 = __toESM(require_src(), 1); +var import_node_util2 = require("util"); +function ht() { + return import_node_process2.default.platform !== "win32" ? import_node_process2.default.env.TERM !== "linux" : !!import_node_process2.default.env.CI || !!import_node_process2.default.env.WT_SESSION || !!import_node_process2.default.env.TERMINUS_SUBLIME || import_node_process2.default.env.ConEmuTask === "{cmd::Cmder}" || import_node_process2.default.env.TERM_PROGRAM === "Terminus-Sublime" || import_node_process2.default.env.TERM_PROGRAM === "vscode" || import_node_process2.default.env.TERM === "xterm-256color" || import_node_process2.default.env.TERM === "alacritty" || import_node_process2.default.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; +} +var ee = ht(); +var w = (e, r) => ee ? e : r; +var Me = w("\u25C6", "*"); +var ce = w("\u25A0", "x"); +var de = w("\u25B2", "x"); +var V = w("\u25C7", "o"); +var $e = w("\u250C", "T"); +var h = w("\u2502", "|"); +var x = w("\u2514", "\u2014"); +var Re = w("\u2510", "T"); +var Oe = w("\u2518", "\u2014"); +var Y = w("\u25CF", ">"); +var K = w("\u25CB", " "); +var te = w("\u25FB", "[\u2022]"); +var k2 = w("\u25FC", "[+]"); +var z = w("\u25FB", "[ ]"); +var Pe = w("\u25AA", "\u2022"); +var se = w("\u2500", "-"); +var he = w("\u256E", "+"); +var Ne = w("\u251C", "+"); +var me = w("\u256F", "+"); +var pe = w("\u2570", "+"); +var We = w("\u256D", "+"); +var ge = w("\u25CF", "\u2022"); +var fe = w("\u25C6", "*"); +var Fe = w("\u25B2", "!"); +var ye = w("\u25A0", "x"); +var Ft2 = { limit: 1 / 0, ellipsis: "" }; +var yt2 = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var Ce = "\x07"; +var Ve = "["; +var vt = "]"; +var we = `${vt}8;;`; +var Ge = new RegExp(`(?:\\${Ve}(?\\d+)m|\\${we}(?.*)${Ce})`, "y"); +var Ut = import_picocolors2.default.magenta; +var Ye = { light: w("\u2500", "-"), heavy: w("\u2501", "="), block: w("\u2588", "#") }; +var ze = `${import_picocolors2.default.gray(h)} `; + +// ../tools/dist/spawn.js +var spawnLogger = logger.child("spawn"); + +// ../tools/dist/react-native.js +var import_node_module = require("module"); +var import_node_path2 = __toESM(require("path"), 1); +var import_node_fs2 = __toESM(require("fs"), 1); + +// ../tools/dist/error.js +var HarnessError = class extends Error { +}; + +// ../tools/dist/packages.js +var import_node_path3 = __toESM(require("path"), 1); +var import_node_fs3 = __toESM(require("fs"), 1); + +// ../tools/dist/path.js +var import_node_path4 = __toESM(require("path"), 1); + +// ../tools/dist/crash-artifacts.js +var import_node_fs4 = __toESM(require("fs"), 1); +var import_node_path5 = __toESM(require("path"), 1); +var DEFAULT_ARTIFACT_ROOT = import_node_path5.default.join(process.cwd(), ".harness", "crash-reports"); + +// ../tools/dist/harness-artifacts.js +var import_node_fs5 = __toESM(require("fs"), 1); +var import_node_path6 = __toESM(require("path"), 1); + +// ../tools/dist/diagnostics.js +var noop = () => void 0; +var noopSpan = { + end: noop, + fail: noop +}; +var createNoopDiagnostics = () => { + const diagnostics = { + enabled: false, + start: () => noopSpan, + measure: async (_label, fn) => fn(), + mark: noop, + record: noop, + child: () => diagnostics, + flush: () => [] + }; + return diagnostics; +}; +var noopDiagnostics = createNoopDiagnostics(); + +// ../cache/dist/paths.js +var import_node_path7 = __toESM(require("path"), 1); +var HARNESS_DIRNAME = ".harness"; +var CACHE_DIRNAME = "cache"; +var METRO_TRANSFORM_DIRNAME = "metro"; +var METRO_FILE_MAP_DIRNAME = "metro-file-map"; +var createHarnessCachePaths = (projectRoot) => { + const root = import_node_path7.default.join(projectRoot, HARNESS_DIRNAME, CACHE_DIRNAME); + return { + root, + metroTransform: import_node_path7.default.join(root, METRO_TRANSFORM_DIRNAME), + metroFileMap: import_node_path7.default.join(root, METRO_FILE_MAP_DIRNAME) + }; +}; +var getDomainDirectories = (domain, paths) => { + switch (domain) { + case "metro": + return [paths.metroTransform, paths.metroFileMap]; + default: + return []; + } +}; + +// ../cache/dist/plan.js +var import_node_crypto = require("crypto"); +var import_node_fs6 = __toESM(require("fs"), 1); +var import_node_path8 = __toESM(require("path"), 1); +var cacheLogger = logger.child("cache"); +var KEYS_FILENAME = "keys.json"; +var HASH_LENGTH = 16; +var ALL_DOMAINS = ["metro"]; +var isAccretiveDomain = (domain) => { + switch (domain) { + case "metro": + return true; + default: + return true; + } +}; +var hashSortedEntries = (entries) => (0, import_node_crypto.createHash)("sha256").update(entries.slice().sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("\n")).digest("hex").slice(0, HASH_LENGTH); +var buildStaticHashKey = (domain, inputs) => { + const hash = hashSortedEntries(Object.entries(inputs.staticInputs)); + return `harness-${domain}-${inputs.os}-${hash}`; +}; +var buildContentHash = (entries) => hashSortedEntries(entries.map((entry) => [entry.path, String(entry.sizeBytes)])); +var walkDirectory = (root, directory, entries) => { + let dirents; + try { + dirents = import_node_fs6.default.readdirSync(directory, { withFileTypes: true }); + } catch { + return; + } + for (const dirent of dirents) { + const fullPath = import_node_path8.default.join(directory, dirent.name); + if (dirent.isDirectory()) { + walkDirectory(root, fullPath, entries); + } else if (dirent.isFile()) { + const stat = import_node_fs6.default.statSync(fullPath); + entries.push({ + path: import_node_path8.default.relative(root, fullPath), + sizeBytes: stat.size, + mtimeMs: stat.mtimeMs + }); + } + } +}; +var planRestore = (paths, inputs) => { + const domains = {}; + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + const restoreKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + domains[domain] = { + paths: [...getDomainDirectories(domain, paths)], + restoreKey, + restorePrefixes: accretive ? [`${restoreKey}-`] : [], + accretive + }; + } + return { version: 1, domains }; +}; +var snapshot = (paths) => { + const result = {}; + for (const domain of ALL_DOMAINS) { + try { + const entries = []; + for (const directory of getDomainDirectories(domain, paths)) { + walkDirectory(paths.root, directory, entries); + } + result[domain] = entries; + } catch (error) { + cacheLogger.warn(`Failed to snapshot cache domain "${domain}". Treating as empty.`, error); + result[domain] = []; + } + } + return result; +}; +var diffSnapshots = (before, after) => { + const beforeByPath = new Map(before.map((entry) => [entry.path, entry])); + const afterByPath = new Map(after.map((entry) => [entry.path, entry])); + let addedEntries = 0; + let removedEntries = 0; + let addedBytes = 0; + for (const [entryPath, afterEntry] of afterByPath) { + const beforeEntry = beforeByPath.get(entryPath); + if (!beforeEntry || beforeEntry.sizeBytes !== afterEntry.sizeBytes) { + addedEntries += 1; + addedBytes += afterEntry.sizeBytes; + } + } + for (const entryPath of beforeByPath.keys()) { + if (!afterByPath.has(entryPath)) { + removedEntries += 1; + } + } + return { addedEntries, removedEntries, addedBytes }; +}; +var shouldSaveForPolicy = (policy, delta) => { + if (delta.addedEntries + delta.removedEntries === 0) { + return false; + } + switch (policy.mode) { + case "always": + return true; + case "default-branch": + return policy.isDefaultBranch; + case "never": + return false; + } +}; +var planSave = (paths, before, policy, inputs) => { + const after = snapshot(paths); + const domains = {}; + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + const beforeEntries = before[domain] ?? []; + const afterEntries = after[domain] ?? []; + const delta = diffSnapshots(beforeEntries, afterEntries); + const staticHashKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + domains[domain] = { + shouldSave: shouldSaveForPolicy(policy, delta), + saveKey: accretive ? `${staticHashKey}-${buildContentHash(afterEntries)}` : staticHashKey, + delta + }; + } + return { version: 1, domains }; +}; +var writeKeysFile = (paths, plan) => { + const filePath = import_node_path8.default.join(paths.root, KEYS_FILENAME); + try { + import_node_fs6.default.mkdirSync(paths.root, { recursive: true }); + import_node_fs6.default.writeFileSync(filePath, JSON.stringify(plan, null, 2)); + } catch (error) { + cacheLogger.warn(`Failed to write cache keys file "${filePath}".`, error); + } +}; + +// ../cache/dist/warmth.js +var import_node_fs7 = __toESM(require("fs"), 1); +var cacheLogger2 = logger.child("cache"); +var isDomainWarm = (domain, paths) => { + const directory = getWarmthDirectory(domain, paths); + if (!directory) { + return false; + } + try { + const stat = import_node_fs7.default.statSync(directory); + if (!stat.isDirectory()) { + return false; + } + return import_node_fs7.default.readdirSync(directory).length > 0; + } catch (error) { + if (isMissingPathError(error)) { + return false; + } + cacheLogger2.warn(`Failed to check cache warmth for "${domain}" at "${directory}". Treating as cold.`, error); + return false; + } +}; +var getWarmthDirectory = (domain, paths) => { + switch (domain) { + case "metro": + return paths.metroTransform; + default: + return void 0; + } +}; +var isMissingPathError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; + +// ../cache/dist/domains/metro.js +var import_node_crypto2 = require("crypto"); +var import_node_fs8 = __toESM(require("fs"), 1); +var import_node_path9 = __toESM(require("path"), 1); +var cacheLogger3 = logger.child("cache"); +var SKIPPED_DIRNAMES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]); +var METRO_KEY_FILENAMES = [ + "bun.lock", + "bun.lockb", + "package-lock.json", + "npm-shrinkwrap.json", + "pnpm-lock.yaml", + "yarn.lock", + "metro.config.js", + "metro.config.cjs", + "metro.config.mjs", + "metro.config.ts", + "babel.config.js", + "babel.config.cjs", + "babel.config.mjs", + "babel.config.ts", + "babel.config.json" +]; +var METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); +var collectKeyFiles = (repoRoot, directory, matches) => { + for (const dirent of import_node_fs8.default.readdirSync(directory, { withFileTypes: true })) { + if (dirent.isDirectory()) { + if (SKIPPED_DIRNAMES.has(dirent.name)) { + continue; + } + collectKeyFiles(repoRoot, import_node_path9.default.join(directory, dirent.name), matches); + } else if (dirent.isFile() && METRO_KEY_FILENAME_SET.has(dirent.name)) { + const fullPath = import_node_path9.default.join(directory, dirent.name); + matches.push({ + relativePath: import_node_path9.default.relative(repoRoot, fullPath).split(import_node_path9.default.sep).join("/"), + content: import_node_fs8.default.readFileSync(fullPath) + }); + } + } +}; +var hashKeyFiles = (matches) => { + const hash = (0, import_node_crypto2.createHash)("sha256"); + for (const match of matches.slice().sort((a, b) => a.relativePath < b.relativePath ? -1 : 1)) { + hash.update(match.relativePath); + hash.update("\0"); + hash.update(match.content); + hash.update("\0"); + } + return hash.digest("hex"); +}; +var computeMetroStaticInputs = (options) => { + try { + const matches = []; + collectKeyFiles(options.repoRoot, options.repoRoot, matches); + return { + lockfileHash: hashKeyFiles(matches), + bundlerMetroVersion: options.bundlerMetroVersion, + ...options.salt ? { salt: options.salt } : {} + }; + } catch (error) { + cacheLogger3.warn(`Failed to compute Metro cache key inputs for "${options.repoRoot}". Falling back to an always-miss key.`, error); + return { + lockfileHash: "unavailable", + bundlerMetroVersion: options.bundlerMetroVersion + }; + } +}; + +// ../cache/dist/index.js +var cacheLogger4 = logger.child("cache"); +var createHarnessCache = (options) => { + const paths = createHarnessCachePaths(options.projectRoot); + return { + paths, + isWarm: (domain) => isDomainWarm(domain, paths), + ensureDomainDirectories: (domain) => { + for (const directory of getDomainDirectories(domain, paths)) { + try { + import_node_fs9.default.mkdirSync(directory, { recursive: true }); + } catch (error) { + cacheLogger4.warn(`Failed to create cache directory "${directory}". Continuing with a cold cache.`, error); + } + } + }, + planRestore: (inputs) => planRestore(paths, inputs), + snapshot: () => snapshot(paths), + planSave: (before, policy, inputs) => planSave(paths, before, policy, inputs), + writeKeysFile: (plan) => writeKeysFile(paths, plan) + }; +}; + +// ../../node_modules/zod/dist/esm/v3/external.js +var external_exports = {}; +__export(external_exports, { + BRAND: () => BRAND, + DIRTY: () => DIRTY, + EMPTY_PATH: () => EMPTY_PATH, + INVALID: () => INVALID, + NEVER: () => NEVER, + OK: () => OK, + ParseStatus: () => ParseStatus, + Schema: () => ZodType, + ZodAny: () => ZodAny, + ZodArray: () => ZodArray, + ZodBigInt: () => ZodBigInt, + ZodBoolean: () => ZodBoolean, + ZodBranded: () => ZodBranded, + ZodCatch: () => ZodCatch, + ZodDate: () => ZodDate, + ZodDefault: () => ZodDefault, + ZodDiscriminatedUnion: () => ZodDiscriminatedUnion, + ZodEffects: () => ZodEffects, + ZodEnum: () => ZodEnum, + ZodError: () => ZodError, + ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind, + ZodFunction: () => ZodFunction, + ZodIntersection: () => ZodIntersection, + ZodIssueCode: () => ZodIssueCode, + ZodLazy: () => ZodLazy, + ZodLiteral: () => ZodLiteral, + ZodMap: () => ZodMap, + ZodNaN: () => ZodNaN, + ZodNativeEnum: () => ZodNativeEnum, + ZodNever: () => ZodNever, + ZodNull: () => ZodNull, + ZodNullable: () => ZodNullable, + ZodNumber: () => ZodNumber, + ZodObject: () => ZodObject, + ZodOptional: () => ZodOptional, + ZodParsedType: () => ZodParsedType, + ZodPipeline: () => ZodPipeline, + ZodPromise: () => ZodPromise, + ZodReadonly: () => ZodReadonly, + ZodRecord: () => ZodRecord, + ZodSchema: () => ZodType, + ZodSet: () => ZodSet, + ZodString: () => ZodString, + ZodSymbol: () => ZodSymbol, + ZodTransformer: () => ZodEffects, + ZodTuple: () => ZodTuple, + ZodType: () => ZodType, + ZodUndefined: () => ZodUndefined, + ZodUnion: () => ZodUnion, + ZodUnknown: () => ZodUnknown, + ZodVoid: () => ZodVoid, + addIssueToContext: () => addIssueToContext, + any: () => anyType, + array: () => arrayType, + bigint: () => bigIntType, + boolean: () => booleanType, + coerce: () => coerce, + custom: () => custom, + date: () => dateType, + datetimeRegex: () => datetimeRegex, + defaultErrorMap: () => en_default, + discriminatedUnion: () => discriminatedUnionType, + effect: () => effectsType, + enum: () => enumType, + function: () => functionType, + getErrorMap: () => getErrorMap, + getParsedType: () => getParsedType, + instanceof: () => instanceOfType, + intersection: () => intersectionType, + isAborted: () => isAborted, + isAsync: () => isAsync, + isDirty: () => isDirty, + isValid: () => isValid, + late: () => late, + lazy: () => lazyType, + literal: () => literalType, + makeIssue: () => makeIssue, + map: () => mapType, + nan: () => nanType, + nativeEnum: () => nativeEnumType, + never: () => neverType, + null: () => nullType, + nullable: () => nullableType, + number: () => numberType, + object: () => objectType, + objectUtil: () => objectUtil, + oboolean: () => oboolean, + onumber: () => onumber, + optional: () => optionalType, + ostring: () => ostring, + pipeline: () => pipelineType, + preprocess: () => preprocessType, + promise: () => promiseType, + quotelessJson: () => quotelessJson, + record: () => recordType, + set: () => setType, + setErrorMap: () => setErrorMap, + strictObject: () => strictObjectType, + string: () => stringType, + symbol: () => symbolType, + transformer: () => effectsType, + tuple: () => tupleType, + undefined: () => undefinedType, + union: () => unionType, + unknown: () => unknownType, + util: () => util2, + void: () => voidType +}); + +// ../../node_modules/zod/dist/esm/v3/helpers/util.js +var util2; +(function(util3) { + util3.assertEqual = (_2) => { + }; + function assertIs(_arg) { + } + util3.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util3.assertNever = assertNever; + util3.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util3.getValidEnumValues = (obj) => { + const validKeys = util3.objectKeys(obj).filter((k3) => typeof obj[obj[k3]] !== "number"); + const filtered = {}; + for (const k3 of validKeys) { + filtered[k3] = obj[k3]; + } + return util3.objectValues(filtered); + }; + util3.objectValues = (obj) => { + return util3.objectKeys(obj).map(function(e) { + return obj[e]; + }); + }; + util3.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util3.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return void 0; + }; + util3.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator); + } + util3.joinValues = joinValues; + util3.jsonStringifyReplacer = (_2, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util2 || (util2 = {})); +var objectUtil; +(function(objectUtil2) { + objectUtil2.mergeShapes = (first, second) => { + return { + ...first, + ...second + // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +var ZodParsedType = util2.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set" +]); +var getParsedType = (data) => { + const t2 = typeof data; + switch (t2) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; + +// ../../node_modules/zod/dist/esm/v3/ZodError.js +var ZodIssueCode = util2.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite" +]); +var quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +var ZodError = class _ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || function(issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + } else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof _ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util2.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +}; +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; + +// ../../node_modules/zod/dist/esm/v3/locales/en.js +var errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util2.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util2.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util2.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util2.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } else { + util2.assertNever(issue.validation); + } + } else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util2.assertNever(issue); + } + return { message }; +}; +var en_default = errorMap; + +// ../../node_modules/zod/dist/esm/v3/errors.js +var overrideErrorMap = en_default; +function setErrorMap(map) { + overrideErrorMap = map; +} +function getErrorMap() { + return overrideErrorMap; +} + +// ../../node_modules/zod/dist/esm/v3/helpers/parseUtil.js +var makeIssue = (params) => { + const { data, path: path12, errorMaps, issueData } = params; + const fullPath = [...path12, ...issueData.path || []]; + const fullIssue = { + ...issueData, + path: fullPath + }; + if (issueData.message !== void 0) { + return { + ...issueData, + path: fullPath, + message: issueData.message + }; + } + let errorMessage = ""; + const maps = errorMaps.filter((m) => !!m).slice().reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage + }; +}; +var EMPTY_PATH = []; +function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, + // contextual error map is first priority + ctx.schemaErrorMap, + // then schema-bound map if available + overrideMap, + // then global override map + overrideMap === en_default ? void 0 : en_default + // then global default map + ].filter((x2) => !!x2) + }); + ctx.common.issues.push(issue); +} +var ParseStatus = class _ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value + }); + } + return _ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +}; +var INVALID = Object.freeze({ + status: "aborted" +}); +var DIRTY = (value) => ({ status: "dirty", value }); +var OK = (value) => ({ status: "valid", value }); +var isAborted = (x2) => x2.status === "aborted"; +var isDirty = (x2) => x2.status === "dirty"; +var isValid = (x2) => x2.status === "valid"; +var isAsync = (x2) => typeof Promise !== "undefined" && x2 instanceof Promise; + +// ../../node_modules/zod/dist/esm/v3/helpers/errorUtil.js +var errorUtil; +(function(errorUtil2) { + errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); + +// ../../node_modules/zod/dist/esm/v3/types.js +var ParseInputLazyPath = class { + constructor(parent, value, path12, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path12; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +}; +var handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + } + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap: errorMap2, invalid_type_error, required_error, description } = params; + if (errorMap2 && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap2) + return { errorMap: errorMap2, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +var ZodType = class { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent + }; + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent + } + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) ? { + value: result.value + } : { + issues: ctx.common.issues + }; + } catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) ? { + value: result.value + } : { + issues: ctx.common.issues + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } else if (typeof message === "function") { + return message(val); + } else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val) + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement } + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data) + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform } + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def) + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(void 0).success; + } + isNullable() { + return this.safeParse(null).success; + } +}; +var cuidRegex = /^c[^\s-]{8,}$/i; +var cuid2Regex = /^[0-9a-z]+$/; +var ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +var uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +var nanoidRegex = /^[a-z0-9_-]{21}$/i; +var jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +var durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +var emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +var _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +var emojiRegex; +var ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +var ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +var ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +var ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +var base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +var base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +var dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +var dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + const base64 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +var ZodString = class _ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx2.parsedType + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = void 0; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message + }); + } else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message + }); + } + status.dirty(); + } + } else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "url") { + try { + new URL(input.data); + } catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "trim") { + input.data = input.data.trim(); + } else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message) + }); + } + _addCheck(check) { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message) + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message) + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message) + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex, + ...errorUtil.errToObj(message) + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value, + position: options?.position, + ...errorUtil.errToObj(options?.message) + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value, + ...errorUtil.errToObj(message) + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value, + ...errorUtil.errToObj(message) + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message) + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message) + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message) + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }] + }); + } + toLowerCase() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }] + }); + } + toUpperCase() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }] + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +}; +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params) + }); +}; +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return valInt % stepInt / 10 ** decCount; +} +var ZodNumber = class _ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx2.parsedType + }); + return INVALID; + } + let ctx = void 0; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util2.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new _ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message) + } + ] + }); + } + _addCheck(check) { + return new _ZodNumber({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message) + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message) + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message) + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message) + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message) + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message) + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message) + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message) + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message) + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || ch.kind === "multipleOf" && util2.isInteger(ch.value)); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +}; +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params) + }); +}; +var ZodBigInt = class _ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = void 0; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new _ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message) + } + ] + }); + } + _addCheck(check) { + return new _ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message) + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message) + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message) + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message) + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message) + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +}; +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params) + }); +}; +var ZodBoolean = class extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params) + }); +}; +var ZodDate = class _ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx2.parsedType + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_date + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = void 0; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date" + }); + status.dirty(); + } + } else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date" + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()) + }; + } + _addCheck(check) { + return new _ZodDate({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message) + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message) + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +}; +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params) + }); +}; +var ZodSymbol = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params) + }); +}; +var ZodUndefined = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params) + }); +}; +var ZodNull = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params) + }); +}; +var ZodAny = class extends ZodType { + constructor() { + super(...arguments); + this._any = true; + } + _parse(input) { + return OK(input.data); + } +}; +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params) + }); +}; +var ZodUnknown = class extends ZodType { + constructor() { + super(...arguments); + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +}; +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params) + }); +}; +var ZodNever = class extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType + }); + return INVALID; + } +}; +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params) + }); +}; +var ZodVoid = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params) + }); +}; +var ZodArray = class _ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: tooSmall ? def.exactLength.value : void 0, + maximum: tooBig ? def.exactLength.value : void 0, + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result2) => { + return ParseStatus.mergeArray(status, result2); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new _ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) } + }); + } + max(maxLength, message) { + return new _ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) } + }); + } + length(len, message) { + return new _ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) } + }); + } + nonempty(message) { + return this.min(1, message); + } +}; +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params) + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape + }); + } else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element) + }); + } else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } else { + return schema; + } +} +var ZodObject = class _ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + this.nonstrict = this.passthrough; + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util2.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx2.parsedType + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] } + }); + } + } else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys + }); + status.dirty(); + } + } else if (unknownKeys === "strip") { + } else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } else { + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse( + new ParseInputLazyPath(ctx, value, ctx.path, key) + //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data + }); + } + } + if (ctx.common.async) { + return Promise.resolve().then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet + }); + } + return syncPairs; + }).then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new _ZodObject({ + ...this._def, + unknownKeys: "strict", + ...message !== void 0 ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError + }; + return { + message: defaultError + }; + } + } : {} + }); + } + strip() { + return new _ZodObject({ + ...this._def, + unknownKeys: "strip" + }); + } + passthrough() { + return new _ZodObject({ + ...this._def, + unknownKeys: "passthrough" + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new _ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation + }) + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new _ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape() + }), + typeName: ZodFirstPartyTypeKind.ZodObject + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new _ZodObject({ + ...this._def, + catchall: index + }); + } + pick(mask) { + const shape = {}; + for (const key of util2.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new _ZodObject({ + ...this._def, + shape: () => shape + }); + } + omit(mask) { + const shape = {}; + for (const key of util2.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new _ZodObject({ + ...this._def, + shape: () => shape + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util2.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } else { + newShape[key] = fieldSchema.optional(); + } + } + return new _ZodObject({ + ...this._def, + shape: () => newShape + }); + } + required(mask) { + const newShape = {}; + for (const key of util2.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new _ZodObject({ + ...this._def, + shape: () => newShape + }); + } + keyof() { + return createZodEnum(util2.objectKeys(this.shape)); + } +}; +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +var ZodUnion = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + }, + parent: null + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx + }), + ctx: childCtx + }; + })).then(handleResults); + } else { + let dirty = void 0; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + }, + parent: null + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx + }); + if (result.status === "valid") { + return result; + } else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues2) => new ZodError(issues2)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +}; +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params) + }); +}; +var getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } else if (type instanceof ZodLiteral) { + return [type.value]; + } else if (type instanceof ZodEnum) { + return type.options; + } else if (type instanceof ZodNativeEnum) { + return util2.objectValues(type.enum); + } else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } else if (type instanceof ZodUndefined) { + return [void 0]; + } else if (type instanceof ZodNull) { + return [null]; + } else if (type instanceof ZodOptional) { + return [void 0, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } else { + return []; + } +}; +var ZodDiscriminatedUnion = class _ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator] + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + } else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + const optionsMap = /* @__PURE__ */ new Map(); + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new _ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params) + }); + } +}; +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util2.objectKeys(b); + const sharedKeys = util2.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } else { + return { valid: false }; + } +} +var ZodIntersection = class extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }) + ]).then(([left, right]) => handleParsed(left, right)); + } else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + })); + } + } +}; +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left, + right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params) + }); +}; +var ZodTuple = class _ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array" + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array" + }); + status.dirty(); + } + const items = [...ctx.data].map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }).filter((x2) => !!x2); + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new _ZodTuple({ + ...this._def, + rest + }); + } +}; +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params) + }); +}; +var ZodRecord = class _ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new _ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third) + }); + } + return new _ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second) + }); + } +}; +var ZodMap = class extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])) + }; + }); + if (ctx.common.async) { + const finalMap = /* @__PURE__ */ new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } else { + const finalMap = /* @__PURE__ */ new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +}; +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params) + }); +}; +var ZodSet = class _ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements2) { + const parsedSet = /* @__PURE__ */ new Set(); + for (const element of elements2) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements2) => finalizeSet(elements2)); + } else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new _ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) } + }); + } + max(maxSize, message) { + return new _ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) } + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +}; +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params) + }); +}; +var ZodFunction = class _ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error + } + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error + } + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + const me2 = this; + return OK(async function(...args) { + const error = new ZodError([]); + const parsedArgs = await me2._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me2._def.returns._def.type.parseAsync(result, params).catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } else { + const me2 = this; + return OK(function(...args) { + const parsedArgs = me2._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me2._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new _ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()) + }); + } + returns(returnType) { + return new _ZodFunction({ + ...this._def, + returns: returnType + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new _ZodFunction({ + args: args ? args : ZodTuple.create([]).rest(ZodUnknown.create()), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params) + }); + } +}; +var ZodLazy = class extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +}; +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params) + }); +}; +var ZodLiteral = class extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +}; +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params) + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params) + }); +} +var ZodEnum = class _ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util2.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return _ZodEnum.create(values, { + ...this._def, + ...newDef + }); + } + exclude(values, newDef = this._def) { + return _ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef + }); + } +}; +ZodEnum.create = createZodEnum; +var ZodNativeEnum = class extends ZodType { + _parse(input) { + const nativeEnumValues = util2.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util2.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util2.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util2.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util2.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +}; +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params) + }); +}; +var ZodPromise = class extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap + }); + })); + } +}; +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params) + }); +}; +var ZodEffects = class extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects ? this._def.schema.sourceType() : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + } + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed2) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed2, + path: ctx.path, + parent: ctx + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result + })); + }); + } + } + util2.assertNever(effect); + } +}; +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params) + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params) + }); +}; +var ZodOptional = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(void 0); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +}; +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params) + }); +}; +var ZodNullable = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +}; +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params) + }); +}; +var ZodDefault = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx + }); + } + removeDefault() { + return this._def.innerType; + } +}; +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params) + }); +}; +var ZodCatch = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + } + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx + } + }); + if (isAsync(result)) { + return result.then((result2) => { + return { + status: "valid", + value: result2.status === "valid" ? result2.value : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data + }) + }; + }); + } else { + return { + status: "valid", + value: result.status === "valid" ? result.value : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data + }) + }; + } + } + removeCatch() { + return this._def.innerType; + } +}; +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params) + }); +}; +var ZodNaN = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +}; +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params) + }); +}; +var BRAND = /* @__PURE__ */ Symbol("zod_brand"); +var ZodBranded = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx + }); + } + unwrap() { + return this._def.type; + } +}; +var ZodPipeline = class _ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx + }); + } + }; + return handleAsync(); + } else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value + }; + } else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx + }); + } + } + } + static create(a, b) { + return new _ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline + }); + } +}; +var ZodReadonly = class extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +}; +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params) + }); +}; +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +function custom(check, _params = {}, fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r2) => { + if (!r2) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +var late = { + object: ZodObject.lazycreate +}; +var ZodFirstPartyTypeKind; +(function(ZodFirstPartyTypeKind2) { + ZodFirstPartyTypeKind2["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +var instanceOfType = (cls, params = { + message: `Input not instance of ${cls.name}` +}) => custom((data) => data instanceof cls, params); +var stringType = ZodString.create; +var numberType = ZodNumber.create; +var nanType = ZodNaN.create; +var bigIntType = ZodBigInt.create; +var booleanType = ZodBoolean.create; +var dateType = ZodDate.create; +var symbolType = ZodSymbol.create; +var undefinedType = ZodUndefined.create; +var nullType = ZodNull.create; +var anyType = ZodAny.create; +var unknownType = ZodUnknown.create; +var neverType = ZodNever.create; +var voidType = ZodVoid.create; +var arrayType = ZodArray.create; +var objectType = ZodObject.create; +var strictObjectType = ZodObject.strictCreate; +var unionType = ZodUnion.create; +var discriminatedUnionType = ZodDiscriminatedUnion.create; +var intersectionType = ZodIntersection.create; +var tupleType = ZodTuple.create; +var recordType = ZodRecord.create; +var mapType = ZodMap.create; +var setType = ZodSet.create; +var functionType = ZodFunction.create; +var lazyType = ZodLazy.create; +var literalType = ZodLiteral.create; +var enumType = ZodEnum.create; +var nativeEnumType = ZodNativeEnum.create; +var promiseType = ZodPromise.create; +var effectsType = ZodEffects.create; +var optionalType = ZodOptional.create; +var nullableType = ZodNullable.create; +var preprocessType = ZodEffects.createWithPreprocess; +var pipelineType = ZodPipeline.create; +var ostring = () => stringType().optional(); +var onumber = () => numberType().optional(); +var oboolean = () => booleanType().optional(); +var coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })) +}; +var NEVER = INVALID; + +// ../plugins/dist/utils.js +var isHookTree = (value) => { + if (value == null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + for (const child of Object.values(value)) { + if (child === void 0) { + continue; + } + if (typeof child === "function") { + continue; + } + if (child == null || typeof child !== "object" || Array.isArray(child) || !isHookTree(child)) { + return false; + } + } + return true; +}; + +// ../plugins/dist/plugin.js +var isHarnessPlugin = (value) => { + if (value == null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const candidate = value; + if (typeof candidate.name !== "string" || candidate.name.length === 0) { + return false; + } + if (candidate.createState != null && typeof candidate.createState !== "function") { + return false; + } + if (candidate.hooks != null && !isHookTree(candidate.hooks)) { + return false; + } + return true; +}; + +// ../plugins/dist/manager.js +var pluginsLogger = logger.child("plugins"); + +// ../config/dist/types.js +var DEFAULT_METRO_PORT = 8081; +var configLogger = logger.child("config"); +var RunnerSchema = external_exports.object({ + name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), + config: external_exports.record(external_exports.any()), + runner: external_exports.string(), + cli: external_exports.string().optional(), + platformId: external_exports.string() +}); +var PluginSchema = external_exports.custom((value) => isHarnessPlugin(value), "Invalid Harness plugin"); +var ConfigSchema = external_exports.object({ + entryPoint: external_exports.string().min(1, "Entry point is required"), + appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"), + runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"), + plugins: external_exports.array(PluginSchema).optional().default([]), + defaultRunner: external_exports.string().optional(), + host: external_exports.string().min(1, "Host is required").optional(), + metroPort: external_exports.number().int("Metro port must be an integer").min(1, "Metro port must be at least 1").max(65535, "Metro port must be at most 65535").optional().default(DEFAULT_METRO_PORT), + webSocketPort: external_exports.number().optional().describe("Deprecated. Bridge traffic now uses metroPort and this value is ignored."), + bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4), + testTimeout: external_exports.number().min(1e3, "Test timeout must be at least 1 second").default(5e3), + platformReadyTimeout: external_exports.number().min(1e3, "Platform ready timeout must be at least 1 second").default(3e5), + bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(6e4), + maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2), + eagerPrewarm: external_exports.boolean().optional().default(true).describe("Start building the Metro bundle while the platform (emulator, simulator, or browser) is still booting, so the first bundle is ready sooner. Disable to defer the first bundle build until app startup."), + resetEnvironmentBetweenTestFiles: external_exports.union([external_exports.boolean(), external_exports.enum(["process", "runtime"])]).optional().default(true).describe("Controls how the environment is reset between test files. `true` (default) and `'process'` kill and cold-restart the app process. `'runtime'` reloads the JS runtime in place (DevSettings.reload() / window.location.reload()), which is cheaper but escalates to a process restart if the reload fails or the app does not reconnect in time. `false` disables resetting the environment between test files entirely."), + skipAlreadyIncludedModules: external_exports.boolean().optional().describe('Skip re-sending modules already served in the main app bundle when Metro serves a per-test-file bundle. Defaults to true; set to false as an escape hatch if it causes issues. Left undefined (rather than defaulted here) so `resolveSkipAlreadyIncludedModules` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__skipAlreadyIncludedModules` alias.'), + // Deprecated alias for `skipAlreadyIncludedModules` -- see + // `resolveSkipAlreadyIncludedModules`. Left without a `.default()` for the + // same reason: a default value would be indistinguishable from the user + // never having set it, which would break alias-vs-explicit-flag precedence. + unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().describe("Deprecated. Use `skipAlreadyIncludedModules` instead."), + cache: external_exports.object({ + metro: external_exports.boolean().optional().describe('Enable persistent Metro transform and file-map caching under the .harness cache directory. Defaults to true; set to false to always start with a cold Metro cache. Left undefined (rather than defaulted here) so `resolveMetroCacheEnabled` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__enableMetroCache` alias.'), + version: external_exports.string().optional().describe("User-controlled salt folded into the Metro cacheVersion. Bump it to force-invalidate previously cached transforms.") + }).optional(), + // Deprecated alias for `cache.metro` -- see `resolveMetroCacheEnabled`. + // Left without a `.default()` for the same reason as + // `unstable__skipAlreadyIncludedModules`: a default value would be + // indistinguishable from the user never having set it, which would break + // alias-vs-explicit-flag precedence. + unstable__enableMetroCache: external_exports.boolean().optional().describe("Deprecated. Use `cache.metro` instead."), + permissions: external_exports.boolean().optional().default(false).describe("Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent."), + detectNativeCrashes: external_exports.boolean().optional().default(true), + crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), + disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."), + coverage: external_exports.object({ + root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`), + native: external_exports.object({ + ios: external_exports.object({ + pods: external_exports.array(external_exports.string()).min(1, "At least one pod name is required").describe("Pod names to instrument for native code coverage. Coverage flags are injected at pod install time via a CocoaPods hook. After tests, profraw data is collected and converted to lcov format.") + }).optional() + }).optional().describe("Native code coverage configuration.") + }).optional(), + forwardClientLogs: external_exports.boolean().optional().default(false).describe("Enable forwarding of console.log, console.warn, console.error, and other console method calls from the React Native app during the active test run. When enabled, app console output is attached to the active test result's console output."), + diagnostics: external_exports.boolean().optional().default(false).describe("Enable diagnostics tracing for the harness session. Records spans for session setup, Metro bundling, bridge/client events, and per-file test runs, then writes a Chrome Trace Event JSON file and prints a summary after each run. Can also be enabled via the RN_HARNESS_DIAGNOSTICS environment variable."), + // Deprecated property - used for migration detection + include: external_exports.array(external_exports.string()).optional() +}).refine((config) => { + if (config.defaultRunner) { + return config.runners.some((runner) => runner.name === config.defaultRunner); + } + return true; +}, { + message: "Default runner must match one of the configured runner names", + path: ["defaultRunner"] +}); + +// ../config/dist/errors.js +var ConfigValidationError = class extends HarnessError { + filePath; + validationErrors; + constructor(filePath, validationErrors) { + const lines = [ + `Configuration validation failed in ${filePath}`, + "", + "The following issues were found:", + "" + ]; + validationErrors.forEach((error, index) => { + lines.push(` ${index + 1}. ${error}`); + }); + lines.push("", "Please fix these issues and try again.", "For more information, visit: https://react-native-harness.dev/docs/configuration"); + super(lines.join("\n")); + this.filePath = filePath; + this.validationErrors = validationErrors; + this.name = "ConfigValidationError"; + } +}; +var ConfigNotFoundError = class extends HarnessError { + searchPath; + constructor(searchPath) { + const lines = [ + "Configuration file not found", + "", + `Searched for configuration files in: ${searchPath}`, + "and all parent directories.", + "", + "React Native Harness looks for one of these files:", + " \u2022 rn-harness.config.js", + " \u2022 rn-harness.config.mjs", + " \u2022 rn-harness.config.cjs", + " \u2022 rn-harness.config.json", + "", + "For more information, visit: https://www.react-native-harness.dev/docs/getting-started/configuration" + ]; + super(lines.join("\n")); + this.searchPath = searchPath; + this.name = "ConfigNotFoundError"; + } +}; +var ConfigLoadError = class extends HarnessError { + filePath; + cause; + constructor(filePath, cause) { + const lines = [ + "Failed to load configuration file", + "", + `File: ${filePath}`, + "" + ]; + if (cause) { + lines.push("Error details:"); + lines.push(` ${cause.message}`); + lines.push(""); + } + lines.push("This could be due to:", " \u2022 Syntax errors in your configuration file", " \u2022 Missing dependencies or modules", " \u2022 Invalid file format or encoding", " \u2022 File permissions issues", "", "Troubleshooting steps:", " 1. Check the file syntax and format", " 2. Ensure all required dependencies are installed", " 3. Verify file permissions", " 4. Try creating a new configuration file", "", "For more help, visit: https://www.react-native-harness.dev/docs/getting-started/configuration"); + super(lines.join("\n")); + this.filePath = filePath; + this.name = "ConfigLoadError"; + this.cause = cause; + } +}; + +// ../config/dist/reader.js +var import_node_path10 = __toESM(require("path"), 1); +var import_node_fs10 = __toESM(require("fs"), 1); +var import_node_module2 = require("module"); +var import_meta = {}; +var extensions = [".js", ".mjs", ".cjs", ".json"]; +var importUp = async (dir, name) => { + const filePath = import_node_path10.default.join(dir, name); + for (const ext of extensions) { + const filePathWithExt = `${filePath}${ext}`; + if (import_node_fs10.default.existsSync(filePathWithExt)) { + let rawConfig; + try { + if (ext === ".mjs") { + rawConfig = await import(filePathWithExt).then((module2) => module2.default); + } else { + const require2 = (0, import_node_module2.createRequire)(import_meta.url); + rawConfig = require2(filePathWithExt); + } + } catch (error) { + throw new ConfigLoadError(filePathWithExt, error instanceof Error ? error : void 0); + } + try { + const config = ConfigSchema.parse(rawConfig); + return { config, filePathWithExt, configDir: dir }; + } catch (error) { + if (error instanceof ZodError) { + const validationErrors = error.errors.map((err) => { + const path12 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : ""; + return `${err.message}${path12}`; + }); + throw new ConfigValidationError(filePathWithExt, validationErrors); + } + throw error; + } + } + } + const parentDir = import_node_path10.default.dirname(dir); + if (parentDir === dir) { + throw new ConfigNotFoundError(dir); + } + return importUp(parentDir, name); +}; +var getConfig = async (dir) => { + const { config, configDir } = await importUp(dir, "rn-harness.config"); + return { + config, + projectRoot: configDir + }; +}; + +// src/shared/metro-cache-inputs.ts +var import_node_fs11 = __toESM(require("fs")); +var import_node_path11 = __toESM(require("path")); +var FALLBACK_BUNDLER_METRO_VERSION = "unknown"; +var resolveRepoRoot = (projectRoot) => { + let dir = projectRoot; + while (true) { + if (import_node_fs11.default.existsSync(import_node_path11.default.join(dir, ".git"))) { + return dir; + } + const parent = import_node_path11.default.dirname(dir); + if (parent === dir) { + return projectRoot; + } + dir = parent; + } +}; +var resolveBundlerMetroVersion = (projectRoot) => { + try { + const packageJsonPath = require.resolve( + "@react-native-harness/bundler-metro/package.json", + { paths: [projectRoot] } + ); + const packageJson = JSON.parse(import_node_fs11.default.readFileSync(packageJsonPath, "utf8")); + if (typeof packageJson.version !== "string") { + throw new Error('"version" field is missing or not a string'); + } + return packageJson.version; + } catch (error) { + console.warn( + `Failed to resolve @react-native-harness/bundler-metro's version from "${projectRoot}". Falling back to "${FALLBACK_BUNDLER_METRO_VERSION}".`, + error + ); + return FALLBACK_BUNDLER_METRO_VERSION; + } +}; +var resolveMetroStaticInputs = (options) => computeMetroStaticInputs({ + repoRoot: resolveRepoRoot(options.projectRoot), + bundlerMetroVersion: resolveBundlerMetroVersion(options.projectRoot), + salt: options.cacheVersionSalt +}); + +// src/shared/plan-restore.ts +var run = async () => { + try { + const projectRootInput = process.env.INPUT_PROJECTROOT; + const projectRoot = projectRootInput ? import_node_path12.default.resolve(projectRootInput) : process.cwd(); + console.info(`Planning Metro cache restore for: ${projectRoot}`); + const { config, projectRoot: resolvedProjectRoot } = await getConfig( + projectRoot + ); + const githubOutput = process.env.GITHUB_OUTPUT; + if (!githubOutput) { + throw new Error("GITHUB_OUTPUT environment variable is not set"); + } + const staticInputs = resolveMetroStaticInputs({ + projectRoot: resolvedProjectRoot, + cacheVersionSalt: config.cache?.version + }); + const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); + const os = process.env.RUNNER_OS ?? process.platform; + const restorePlan = cache.planRestore({ + metro: { os, staticInputs } + }); + cache.writeKeysFile(restorePlan); + const metroPlan = restorePlan.domains.metro; + const output = `metroRestoreKey=${metroPlan?.restoreKey ?? ""} +metroRestorePrefixes=${JSON.stringify( + metroPlan?.restorePrefixes ?? [] + )} +`; + import_node_fs12.default.appendFileSync(githubOutput, output); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } else { + console.error("Failed to plan Metro cache restore"); + } + process.exit(1); + } +}; +run(); diff --git a/actions/shared/plan-save.cjs b/actions/shared/plan-save.cjs new file mode 100644 index 00000000..ab7de645 --- /dev/null +++ b/actions/shared/plan-save.cjs @@ -0,0 +1,5004 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// ../../node_modules/picocolors/picocolors.js +var require_picocolors = __commonJS({ + "../../node_modules/picocolors/picocolors.js"(exports2, module2) { + "use strict"; + var p = process || {}; + var argv = p.argv || []; + var env = p.env || {}; + var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI); + var formatter = (open, close, replace = open) => (input) => { + let string = "" + input, index = string.indexOf(close, open.length); + return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; + }; + var replaceClose = (string, close, replace, index) => { + let result = "", cursor = 0; + do { + result += string.substring(cursor, index) + replace; + cursor = index + close.length; + index = string.indexOf(close, cursor); + } while (~index); + return result + string.substring(cursor); + }; + var createColors = (enabled = isColorSupported) => { + let f = enabled ? formatter : () => String; + return { + isColorSupported: enabled, + reset: f("\x1B[0m", "\x1B[0m"), + bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"), + dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"), + italic: f("\x1B[3m", "\x1B[23m"), + underline: f("\x1B[4m", "\x1B[24m"), + inverse: f("\x1B[7m", "\x1B[27m"), + hidden: f("\x1B[8m", "\x1B[28m"), + strikethrough: f("\x1B[9m", "\x1B[29m"), + black: f("\x1B[30m", "\x1B[39m"), + red: f("\x1B[31m", "\x1B[39m"), + green: f("\x1B[32m", "\x1B[39m"), + yellow: f("\x1B[33m", "\x1B[39m"), + blue: f("\x1B[34m", "\x1B[39m"), + magenta: f("\x1B[35m", "\x1B[39m"), + cyan: f("\x1B[36m", "\x1B[39m"), + white: f("\x1B[37m", "\x1B[39m"), + gray: f("\x1B[90m", "\x1B[39m"), + bgBlack: f("\x1B[40m", "\x1B[49m"), + bgRed: f("\x1B[41m", "\x1B[49m"), + bgGreen: f("\x1B[42m", "\x1B[49m"), + bgYellow: f("\x1B[43m", "\x1B[49m"), + bgBlue: f("\x1B[44m", "\x1B[49m"), + bgMagenta: f("\x1B[45m", "\x1B[49m"), + bgCyan: f("\x1B[46m", "\x1B[49m"), + bgWhite: f("\x1B[47m", "\x1B[49m"), + blackBright: f("\x1B[90m", "\x1B[39m"), + redBright: f("\x1B[91m", "\x1B[39m"), + greenBright: f("\x1B[92m", "\x1B[39m"), + yellowBright: f("\x1B[93m", "\x1B[39m"), + blueBright: f("\x1B[94m", "\x1B[39m"), + magentaBright: f("\x1B[95m", "\x1B[39m"), + cyanBright: f("\x1B[96m", "\x1B[39m"), + whiteBright: f("\x1B[97m", "\x1B[39m"), + bgBlackBright: f("\x1B[100m", "\x1B[49m"), + bgRedBright: f("\x1B[101m", "\x1B[49m"), + bgGreenBright: f("\x1B[102m", "\x1B[49m"), + bgYellowBright: f("\x1B[103m", "\x1B[49m"), + bgBlueBright: f("\x1B[104m", "\x1B[49m"), + bgMagentaBright: f("\x1B[105m", "\x1B[49m"), + bgCyanBright: f("\x1B[106m", "\x1B[49m"), + bgWhiteBright: f("\x1B[107m", "\x1B[49m") + }; + }; + module2.exports = createColors(); + module2.exports.createColors = createColors; + } +}); + +// ../../node_modules/sisteransi/src/index.js +var require_src = __commonJS({ + "../../node_modules/sisteransi/src/index.js"(exports2, module2) { + "use strict"; + var ESC = "\x1B"; + var CSI = `${ESC}[`; + var beep = "\x07"; + var cursor = { + to(x2, y) { + if (!y) return `${CSI}${x2 + 1}G`; + return `${CSI}${y + 1};${x2 + 1}H`; + }, + move(x2, y) { + let ret = ""; + if (x2 < 0) ret += `${CSI}${-x2}D`; + else if (x2 > 0) ret += `${CSI}${x2}C`; + if (y < 0) ret += `${CSI}${-y}A`; + else if (y > 0) ret += `${CSI}${y}B`; + return ret; + }, + up: (count = 1) => `${CSI}${count}A`, + down: (count = 1) => `${CSI}${count}B`, + forward: (count = 1) => `${CSI}${count}C`, + backward: (count = 1) => `${CSI}${count}D`, + nextLine: (count = 1) => `${CSI}E`.repeat(count), + prevLine: (count = 1) => `${CSI}F`.repeat(count), + left: `${CSI}G`, + hide: `${CSI}?25l`, + show: `${CSI}?25h`, + save: `${ESC}7`, + restore: `${ESC}8` + }; + var scroll = { + up: (count = 1) => `${CSI}S`.repeat(count), + down: (count = 1) => `${CSI}T`.repeat(count) + }; + var erase = { + screen: `${CSI}2J`, + up: (count = 1) => `${CSI}1J`.repeat(count), + down: (count = 1) => `${CSI}J`.repeat(count), + line: `${CSI}2K`, + lineEnd: `${CSI}K`, + lineStart: `${CSI}1K`, + lines(count) { + let clear = ""; + for (let i = 0; i < count; i++) + clear += this.line + (i < count - 1 ? cursor.up() : ""); + if (count) + clear += cursor.left; + return clear; + } + }; + module2.exports = { cursor, scroll, erase, beep }; + } +}); + +// src/shared/plan-save.ts +var import_node_fs12 = __toESM(require("fs")); +var import_node_path12 = __toESM(require("path")); + +// ../cache/dist/index.js +var import_node_fs9 = __toESM(require("fs"), 1); + +// ../tools/dist/net.js +var import_node_net = __toESM(require("net"), 1); + +// ../tools/dist/logger.js +var import_node_util = __toESM(require("util"), 1); +var import_picocolors = __toESM(require_picocolors(), 1); +var verbose = !!process.env.HARNESS_DEBUG; +var BASE_TAG = "[harness]"; +var INFO_TAG = import_picocolors.default.isColorSupported ? import_picocolors.default.reset(import_picocolors.default.inverse(import_picocolors.default.bold(import_picocolors.default.magenta(" HARNESS ")))) : "HARNESS"; +var ERROR_TAG = import_picocolors.default.isColorSupported ? import_picocolors.default.reset(import_picocolors.default.inverse(import_picocolors.default.bold(import_picocolors.default.red(" HARNESS ")))) : "HARNESS"; +var getTimestamp = () => (/* @__PURE__ */ new Date()).toISOString(); +var normalizeScope = (scope) => scope.trim().replace(/^\[+|\]+$/g, "").replace(/\]\[/g, "]["); +var formatPrefix = (scopes) => { + const suffix = scopes.map((scope) => `[${normalizeScope(scope)}]`).join(""); + return `${BASE_TAG}${suffix}`; +}; +var mapLines = (text, prefix) => text.split("\n").map((line) => `${prefix} ${line}`).join("\n"); +var writeLog = (level, scopes, messages) => { + if (!verbose && (level === "info" || level === "log" || level === "success")) { + const output2 = import_node_util.default.format(...messages); + const tag = INFO_TAG; + process.stderr.write(`${tag} ${output2} +`); + return; + } + if (!verbose && level === "error") { + const output2 = import_node_util.default.format(...messages); + process.stderr.write(`${ERROR_TAG} ${output2} +`); + return; + } + const method = level === "warn" ? console.warn : console.debug; + const output = import_node_util.default.format(...messages); + const prefix = `${getTimestamp()} ${formatPrefix(scopes)}`; + method(mapLines(output, prefix)); +}; +var setVerbose = (level) => { + verbose = level; +}; +var isVerbose = () => { + return verbose; +}; +var createScopedLogger = (scopes = []) => ({ + debug: (...messages) => { + if (!verbose) { + return; + } + writeLog("debug", scopes, messages); + }, + info: (...messages) => { + writeLog("info", scopes, messages); + }, + warn: (...messages) => { + writeLog("warn", scopes, messages); + }, + error: (...messages) => { + writeLog("error", scopes, messages); + }, + log: (...messages) => { + writeLog("log", scopes, messages); + }, + success: (...messages) => { + writeLog("success", scopes, messages); + }, + child: (scope) => createScopedLogger([...scopes, scope]), + setVerbose, + isVerbose +}); +var logger = createScopedLogger(); + +// ../../node_modules/@clack/core/dist/index.mjs +var import_node_process = require("process"); +var k = __toESM(require("readline"), 1); +var import_node_readline = __toESM(require("readline"), 1); +var import_sisteransi = __toESM(require_src(), 1); +var import_node_tty = require("tty"); +var Ft = { limit: 1 / 0, ellipsis: "" }; +var ft = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var j = "\x07"; +var Q = "["; +var dt = "]"; +var U = `${dt}8;;`; +var et = new RegExp(`(?:\\${Q}(?\\d+)m|\\${U}(?.*)${j})`, "y"); +var At = ["up", "down", "left", "right", "space", "enter", "cancel"]; +var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true }; +var bt = globalThis.process.platform.startsWith("win"); + +// ../../node_modules/@clack/prompts/dist/index.mjs +var import_picocolors2 = __toESM(require_picocolors(), 1); +var import_node_process2 = __toESM(require("process"), 1); +var import_node_fs = require("fs"); +var import_node_path = require("path"); +var import_sisteransi2 = __toESM(require_src(), 1); +var import_node_util2 = require("util"); +function ht() { + return import_node_process2.default.platform !== "win32" ? import_node_process2.default.env.TERM !== "linux" : !!import_node_process2.default.env.CI || !!import_node_process2.default.env.WT_SESSION || !!import_node_process2.default.env.TERMINUS_SUBLIME || import_node_process2.default.env.ConEmuTask === "{cmd::Cmder}" || import_node_process2.default.env.TERM_PROGRAM === "Terminus-Sublime" || import_node_process2.default.env.TERM_PROGRAM === "vscode" || import_node_process2.default.env.TERM === "xterm-256color" || import_node_process2.default.env.TERM === "alacritty" || import_node_process2.default.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; +} +var ee = ht(); +var w = (e, r) => ee ? e : r; +var Me = w("\u25C6", "*"); +var ce = w("\u25A0", "x"); +var de = w("\u25B2", "x"); +var V = w("\u25C7", "o"); +var $e = w("\u250C", "T"); +var h = w("\u2502", "|"); +var x = w("\u2514", "\u2014"); +var Re = w("\u2510", "T"); +var Oe = w("\u2518", "\u2014"); +var Y = w("\u25CF", ">"); +var K = w("\u25CB", " "); +var te = w("\u25FB", "[\u2022]"); +var k2 = w("\u25FC", "[+]"); +var z = w("\u25FB", "[ ]"); +var Pe = w("\u25AA", "\u2022"); +var se = w("\u2500", "-"); +var he = w("\u256E", "+"); +var Ne = w("\u251C", "+"); +var me = w("\u256F", "+"); +var pe = w("\u2570", "+"); +var We = w("\u256D", "+"); +var ge = w("\u25CF", "\u2022"); +var fe = w("\u25C6", "*"); +var Fe = w("\u25B2", "!"); +var ye = w("\u25A0", "x"); +var Ft2 = { limit: 1 / 0, ellipsis: "" }; +var yt2 = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var Ce = "\x07"; +var Ve = "["; +var vt = "]"; +var we = `${vt}8;;`; +var Ge = new RegExp(`(?:\\${Ve}(?\\d+)m|\\${we}(?.*)${Ce})`, "y"); +var Ut = import_picocolors2.default.magenta; +var Ye = { light: w("\u2500", "-"), heavy: w("\u2501", "="), block: w("\u2588", "#") }; +var ze = `${import_picocolors2.default.gray(h)} `; + +// ../tools/dist/spawn.js +var spawnLogger = logger.child("spawn"); + +// ../tools/dist/react-native.js +var import_node_module = require("module"); +var import_node_path2 = __toESM(require("path"), 1); +var import_node_fs2 = __toESM(require("fs"), 1); + +// ../tools/dist/error.js +var HarnessError = class extends Error { +}; + +// ../tools/dist/packages.js +var import_node_path3 = __toESM(require("path"), 1); +var import_node_fs3 = __toESM(require("fs"), 1); + +// ../tools/dist/path.js +var import_node_path4 = __toESM(require("path"), 1); + +// ../tools/dist/crash-artifacts.js +var import_node_fs4 = __toESM(require("fs"), 1); +var import_node_path5 = __toESM(require("path"), 1); +var DEFAULT_ARTIFACT_ROOT = import_node_path5.default.join(process.cwd(), ".harness", "crash-reports"); + +// ../tools/dist/harness-artifacts.js +var import_node_fs5 = __toESM(require("fs"), 1); +var import_node_path6 = __toESM(require("path"), 1); + +// ../tools/dist/diagnostics.js +var noop = () => void 0; +var noopSpan = { + end: noop, + fail: noop +}; +var createNoopDiagnostics = () => { + const diagnostics = { + enabled: false, + start: () => noopSpan, + measure: async (_label, fn) => fn(), + mark: noop, + record: noop, + child: () => diagnostics, + flush: () => [] + }; + return diagnostics; +}; +var noopDiagnostics = createNoopDiagnostics(); + +// ../cache/dist/paths.js +var import_node_path7 = __toESM(require("path"), 1); +var HARNESS_DIRNAME = ".harness"; +var CACHE_DIRNAME = "cache"; +var METRO_TRANSFORM_DIRNAME = "metro"; +var METRO_FILE_MAP_DIRNAME = "metro-file-map"; +var createHarnessCachePaths = (projectRoot) => { + const root = import_node_path7.default.join(projectRoot, HARNESS_DIRNAME, CACHE_DIRNAME); + return { + root, + metroTransform: import_node_path7.default.join(root, METRO_TRANSFORM_DIRNAME), + metroFileMap: import_node_path7.default.join(root, METRO_FILE_MAP_DIRNAME) + }; +}; +var getDomainDirectories = (domain, paths) => { + switch (domain) { + case "metro": + return [paths.metroTransform, paths.metroFileMap]; + default: + return []; + } +}; + +// ../cache/dist/plan.js +var import_node_crypto = require("crypto"); +var import_node_fs6 = __toESM(require("fs"), 1); +var import_node_path8 = __toESM(require("path"), 1); +var cacheLogger = logger.child("cache"); +var KEYS_FILENAME = "keys.json"; +var HASH_LENGTH = 16; +var ALL_DOMAINS = ["metro"]; +var isAccretiveDomain = (domain) => { + switch (domain) { + case "metro": + return true; + default: + return true; + } +}; +var hashSortedEntries = (entries) => (0, import_node_crypto.createHash)("sha256").update(entries.slice().sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("\n")).digest("hex").slice(0, HASH_LENGTH); +var buildStaticHashKey = (domain, inputs) => { + const hash = hashSortedEntries(Object.entries(inputs.staticInputs)); + return `harness-${domain}-${inputs.os}-${hash}`; +}; +var buildContentHash = (entries) => hashSortedEntries(entries.map((entry) => [entry.path, String(entry.sizeBytes)])); +var walkDirectory = (root, directory, entries) => { + let dirents; + try { + dirents = import_node_fs6.default.readdirSync(directory, { withFileTypes: true }); + } catch { + return; + } + for (const dirent of dirents) { + const fullPath = import_node_path8.default.join(directory, dirent.name); + if (dirent.isDirectory()) { + walkDirectory(root, fullPath, entries); + } else if (dirent.isFile()) { + const stat = import_node_fs6.default.statSync(fullPath); + entries.push({ + path: import_node_path8.default.relative(root, fullPath), + sizeBytes: stat.size, + mtimeMs: stat.mtimeMs + }); + } + } +}; +var planRestore = (paths, inputs) => { + const domains = {}; + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + const restoreKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + domains[domain] = { + paths: [...getDomainDirectories(domain, paths)], + restoreKey, + restorePrefixes: accretive ? [`${restoreKey}-`] : [], + accretive + }; + } + return { version: 1, domains }; +}; +var snapshot = (paths) => { + const result = {}; + for (const domain of ALL_DOMAINS) { + try { + const entries = []; + for (const directory of getDomainDirectories(domain, paths)) { + walkDirectory(paths.root, directory, entries); + } + result[domain] = entries; + } catch (error) { + cacheLogger.warn(`Failed to snapshot cache domain "${domain}". Treating as empty.`, error); + result[domain] = []; + } + } + return result; +}; +var diffSnapshots = (before, after) => { + const beforeByPath = new Map(before.map((entry) => [entry.path, entry])); + const afterByPath = new Map(after.map((entry) => [entry.path, entry])); + let addedEntries = 0; + let removedEntries = 0; + let addedBytes = 0; + for (const [entryPath, afterEntry] of afterByPath) { + const beforeEntry = beforeByPath.get(entryPath); + if (!beforeEntry || beforeEntry.sizeBytes !== afterEntry.sizeBytes) { + addedEntries += 1; + addedBytes += afterEntry.sizeBytes; + } + } + for (const entryPath of beforeByPath.keys()) { + if (!afterByPath.has(entryPath)) { + removedEntries += 1; + } + } + return { addedEntries, removedEntries, addedBytes }; +}; +var shouldSaveForPolicy = (policy, delta) => { + if (delta.addedEntries + delta.removedEntries === 0) { + return false; + } + switch (policy.mode) { + case "always": + return true; + case "default-branch": + return policy.isDefaultBranch; + case "never": + return false; + } +}; +var planSave = (paths, before, policy, inputs) => { + const after = snapshot(paths); + const domains = {}; + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + const beforeEntries = before[domain] ?? []; + const afterEntries = after[domain] ?? []; + const delta = diffSnapshots(beforeEntries, afterEntries); + const staticHashKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + domains[domain] = { + shouldSave: shouldSaveForPolicy(policy, delta), + saveKey: accretive ? `${staticHashKey}-${buildContentHash(afterEntries)}` : staticHashKey, + delta + }; + } + return { version: 1, domains }; +}; +var writeKeysFile = (paths, plan) => { + const filePath = import_node_path8.default.join(paths.root, KEYS_FILENAME); + try { + import_node_fs6.default.mkdirSync(paths.root, { recursive: true }); + import_node_fs6.default.writeFileSync(filePath, JSON.stringify(plan, null, 2)); + } catch (error) { + cacheLogger.warn(`Failed to write cache keys file "${filePath}".`, error); + } +}; + +// ../cache/dist/warmth.js +var import_node_fs7 = __toESM(require("fs"), 1); +var cacheLogger2 = logger.child("cache"); +var isDomainWarm = (domain, paths) => { + const directory = getWarmthDirectory(domain, paths); + if (!directory) { + return false; + } + try { + const stat = import_node_fs7.default.statSync(directory); + if (!stat.isDirectory()) { + return false; + } + return import_node_fs7.default.readdirSync(directory).length > 0; + } catch (error) { + if (isMissingPathError(error)) { + return false; + } + cacheLogger2.warn(`Failed to check cache warmth for "${domain}" at "${directory}". Treating as cold.`, error); + return false; + } +}; +var getWarmthDirectory = (domain, paths) => { + switch (domain) { + case "metro": + return paths.metroTransform; + default: + return void 0; + } +}; +var isMissingPathError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; + +// ../cache/dist/domains/metro.js +var import_node_crypto2 = require("crypto"); +var import_node_fs8 = __toESM(require("fs"), 1); +var import_node_path9 = __toESM(require("path"), 1); +var cacheLogger3 = logger.child("cache"); +var SKIPPED_DIRNAMES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]); +var METRO_KEY_FILENAMES = [ + "bun.lock", + "bun.lockb", + "package-lock.json", + "npm-shrinkwrap.json", + "pnpm-lock.yaml", + "yarn.lock", + "metro.config.js", + "metro.config.cjs", + "metro.config.mjs", + "metro.config.ts", + "babel.config.js", + "babel.config.cjs", + "babel.config.mjs", + "babel.config.ts", + "babel.config.json" +]; +var METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); +var collectKeyFiles = (repoRoot, directory, matches) => { + for (const dirent of import_node_fs8.default.readdirSync(directory, { withFileTypes: true })) { + if (dirent.isDirectory()) { + if (SKIPPED_DIRNAMES.has(dirent.name)) { + continue; + } + collectKeyFiles(repoRoot, import_node_path9.default.join(directory, dirent.name), matches); + } else if (dirent.isFile() && METRO_KEY_FILENAME_SET.has(dirent.name)) { + const fullPath = import_node_path9.default.join(directory, dirent.name); + matches.push({ + relativePath: import_node_path9.default.relative(repoRoot, fullPath).split(import_node_path9.default.sep).join("/"), + content: import_node_fs8.default.readFileSync(fullPath) + }); + } + } +}; +var hashKeyFiles = (matches) => { + const hash = (0, import_node_crypto2.createHash)("sha256"); + for (const match of matches.slice().sort((a, b) => a.relativePath < b.relativePath ? -1 : 1)) { + hash.update(match.relativePath); + hash.update("\0"); + hash.update(match.content); + hash.update("\0"); + } + return hash.digest("hex"); +}; +var computeMetroStaticInputs = (options) => { + try { + const matches = []; + collectKeyFiles(options.repoRoot, options.repoRoot, matches); + return { + lockfileHash: hashKeyFiles(matches), + bundlerMetroVersion: options.bundlerMetroVersion, + ...options.salt ? { salt: options.salt } : {} + }; + } catch (error) { + cacheLogger3.warn(`Failed to compute Metro cache key inputs for "${options.repoRoot}". Falling back to an always-miss key.`, error); + return { + lockfileHash: "unavailable", + bundlerMetroVersion: options.bundlerMetroVersion + }; + } +}; + +// ../cache/dist/index.js +var cacheLogger4 = logger.child("cache"); +var createHarnessCache = (options) => { + const paths = createHarnessCachePaths(options.projectRoot); + return { + paths, + isWarm: (domain) => isDomainWarm(domain, paths), + ensureDomainDirectories: (domain) => { + for (const directory of getDomainDirectories(domain, paths)) { + try { + import_node_fs9.default.mkdirSync(directory, { recursive: true }); + } catch (error) { + cacheLogger4.warn(`Failed to create cache directory "${directory}". Continuing with a cold cache.`, error); + } + } + }, + planRestore: (inputs) => planRestore(paths, inputs), + snapshot: () => snapshot(paths), + planSave: (before, policy, inputs) => planSave(paths, before, policy, inputs), + writeKeysFile: (plan) => writeKeysFile(paths, plan) + }; +}; + +// ../../node_modules/zod/dist/esm/v3/external.js +var external_exports = {}; +__export(external_exports, { + BRAND: () => BRAND, + DIRTY: () => DIRTY, + EMPTY_PATH: () => EMPTY_PATH, + INVALID: () => INVALID, + NEVER: () => NEVER, + OK: () => OK, + ParseStatus: () => ParseStatus, + Schema: () => ZodType, + ZodAny: () => ZodAny, + ZodArray: () => ZodArray, + ZodBigInt: () => ZodBigInt, + ZodBoolean: () => ZodBoolean, + ZodBranded: () => ZodBranded, + ZodCatch: () => ZodCatch, + ZodDate: () => ZodDate, + ZodDefault: () => ZodDefault, + ZodDiscriminatedUnion: () => ZodDiscriminatedUnion, + ZodEffects: () => ZodEffects, + ZodEnum: () => ZodEnum, + ZodError: () => ZodError, + ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind, + ZodFunction: () => ZodFunction, + ZodIntersection: () => ZodIntersection, + ZodIssueCode: () => ZodIssueCode, + ZodLazy: () => ZodLazy, + ZodLiteral: () => ZodLiteral, + ZodMap: () => ZodMap, + ZodNaN: () => ZodNaN, + ZodNativeEnum: () => ZodNativeEnum, + ZodNever: () => ZodNever, + ZodNull: () => ZodNull, + ZodNullable: () => ZodNullable, + ZodNumber: () => ZodNumber, + ZodObject: () => ZodObject, + ZodOptional: () => ZodOptional, + ZodParsedType: () => ZodParsedType, + ZodPipeline: () => ZodPipeline, + ZodPromise: () => ZodPromise, + ZodReadonly: () => ZodReadonly, + ZodRecord: () => ZodRecord, + ZodSchema: () => ZodType, + ZodSet: () => ZodSet, + ZodString: () => ZodString, + ZodSymbol: () => ZodSymbol, + ZodTransformer: () => ZodEffects, + ZodTuple: () => ZodTuple, + ZodType: () => ZodType, + ZodUndefined: () => ZodUndefined, + ZodUnion: () => ZodUnion, + ZodUnknown: () => ZodUnknown, + ZodVoid: () => ZodVoid, + addIssueToContext: () => addIssueToContext, + any: () => anyType, + array: () => arrayType, + bigint: () => bigIntType, + boolean: () => booleanType, + coerce: () => coerce, + custom: () => custom, + date: () => dateType, + datetimeRegex: () => datetimeRegex, + defaultErrorMap: () => en_default, + discriminatedUnion: () => discriminatedUnionType, + effect: () => effectsType, + enum: () => enumType, + function: () => functionType, + getErrorMap: () => getErrorMap, + getParsedType: () => getParsedType, + instanceof: () => instanceOfType, + intersection: () => intersectionType, + isAborted: () => isAborted, + isAsync: () => isAsync, + isDirty: () => isDirty, + isValid: () => isValid, + late: () => late, + lazy: () => lazyType, + literal: () => literalType, + makeIssue: () => makeIssue, + map: () => mapType, + nan: () => nanType, + nativeEnum: () => nativeEnumType, + never: () => neverType, + null: () => nullType, + nullable: () => nullableType, + number: () => numberType, + object: () => objectType, + objectUtil: () => objectUtil, + oboolean: () => oboolean, + onumber: () => onumber, + optional: () => optionalType, + ostring: () => ostring, + pipeline: () => pipelineType, + preprocess: () => preprocessType, + promise: () => promiseType, + quotelessJson: () => quotelessJson, + record: () => recordType, + set: () => setType, + setErrorMap: () => setErrorMap, + strictObject: () => strictObjectType, + string: () => stringType, + symbol: () => symbolType, + transformer: () => effectsType, + tuple: () => tupleType, + undefined: () => undefinedType, + union: () => unionType, + unknown: () => unknownType, + util: () => util2, + void: () => voidType +}); + +// ../../node_modules/zod/dist/esm/v3/helpers/util.js +var util2; +(function(util3) { + util3.assertEqual = (_2) => { + }; + function assertIs(_arg) { + } + util3.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util3.assertNever = assertNever; + util3.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util3.getValidEnumValues = (obj) => { + const validKeys = util3.objectKeys(obj).filter((k3) => typeof obj[obj[k3]] !== "number"); + const filtered = {}; + for (const k3 of validKeys) { + filtered[k3] = obj[k3]; + } + return util3.objectValues(filtered); + }; + util3.objectValues = (obj) => { + return util3.objectKeys(obj).map(function(e) { + return obj[e]; + }); + }; + util3.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util3.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return void 0; + }; + util3.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator); + } + util3.joinValues = joinValues; + util3.jsonStringifyReplacer = (_2, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util2 || (util2 = {})); +var objectUtil; +(function(objectUtil2) { + objectUtil2.mergeShapes = (first, second) => { + return { + ...first, + ...second + // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +var ZodParsedType = util2.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set" +]); +var getParsedType = (data) => { + const t2 = typeof data; + switch (t2) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; + +// ../../node_modules/zod/dist/esm/v3/ZodError.js +var ZodIssueCode = util2.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite" +]); +var quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +var ZodError = class _ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || function(issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + } else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof _ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util2.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +}; +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; + +// ../../node_modules/zod/dist/esm/v3/locales/en.js +var errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util2.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util2.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util2.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util2.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } else { + util2.assertNever(issue.validation); + } + } else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util2.assertNever(issue); + } + return { message }; +}; +var en_default = errorMap; + +// ../../node_modules/zod/dist/esm/v3/errors.js +var overrideErrorMap = en_default; +function setErrorMap(map) { + overrideErrorMap = map; +} +function getErrorMap() { + return overrideErrorMap; +} + +// ../../node_modules/zod/dist/esm/v3/helpers/parseUtil.js +var makeIssue = (params) => { + const { data, path: path12, errorMaps, issueData } = params; + const fullPath = [...path12, ...issueData.path || []]; + const fullIssue = { + ...issueData, + path: fullPath + }; + if (issueData.message !== void 0) { + return { + ...issueData, + path: fullPath, + message: issueData.message + }; + } + let errorMessage = ""; + const maps = errorMaps.filter((m) => !!m).slice().reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage + }; +}; +var EMPTY_PATH = []; +function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, + // contextual error map is first priority + ctx.schemaErrorMap, + // then schema-bound map if available + overrideMap, + // then global override map + overrideMap === en_default ? void 0 : en_default + // then global default map + ].filter((x2) => !!x2) + }); + ctx.common.issues.push(issue); +} +var ParseStatus = class _ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value + }); + } + return _ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +}; +var INVALID = Object.freeze({ + status: "aborted" +}); +var DIRTY = (value) => ({ status: "dirty", value }); +var OK = (value) => ({ status: "valid", value }); +var isAborted = (x2) => x2.status === "aborted"; +var isDirty = (x2) => x2.status === "dirty"; +var isValid = (x2) => x2.status === "valid"; +var isAsync = (x2) => typeof Promise !== "undefined" && x2 instanceof Promise; + +// ../../node_modules/zod/dist/esm/v3/helpers/errorUtil.js +var errorUtil; +(function(errorUtil2) { + errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); + +// ../../node_modules/zod/dist/esm/v3/types.js +var ParseInputLazyPath = class { + constructor(parent, value, path12, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path12; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +}; +var handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + } + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap: errorMap2, invalid_type_error, required_error, description } = params; + if (errorMap2 && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap2) + return { errorMap: errorMap2, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +var ZodType = class { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent + }; + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent + } + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) ? { + value: result.value + } : { + issues: ctx.common.issues + }; + } catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) ? { + value: result.value + } : { + issues: ctx.common.issues + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } else if (typeof message === "function") { + return message(val); + } else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val) + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement } + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data) + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform } + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def) + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(void 0).success; + } + isNullable() { + return this.safeParse(null).success; + } +}; +var cuidRegex = /^c[^\s-]{8,}$/i; +var cuid2Regex = /^[0-9a-z]+$/; +var ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +var uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +var nanoidRegex = /^[a-z0-9_-]{21}$/i; +var jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +var durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +var emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +var _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +var emojiRegex; +var ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +var ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +var ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +var ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +var base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +var base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +var dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +var dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + const base64 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +var ZodString = class _ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx2.parsedType + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = void 0; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message + }); + } else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message + }); + } + status.dirty(); + } + } else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "url") { + try { + new URL(input.data); + } catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "trim") { + input.data = input.data.trim(); + } else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message) + }); + } + _addCheck(check) { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message) + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message) + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message) + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex, + ...errorUtil.errToObj(message) + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value, + position: options?.position, + ...errorUtil.errToObj(options?.message) + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value, + ...errorUtil.errToObj(message) + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value, + ...errorUtil.errToObj(message) + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message) + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message) + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message) + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }] + }); + } + toLowerCase() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }] + }); + } + toUpperCase() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }] + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +}; +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params) + }); +}; +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return valInt % stepInt / 10 ** decCount; +} +var ZodNumber = class _ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx2.parsedType + }); + return INVALID; + } + let ctx = void 0; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util2.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new _ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message) + } + ] + }); + } + _addCheck(check) { + return new _ZodNumber({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message) + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message) + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message) + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message) + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message) + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message) + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message) + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message) + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message) + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || ch.kind === "multipleOf" && util2.isInteger(ch.value)); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +}; +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params) + }); +}; +var ZodBigInt = class _ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = void 0; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new _ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message) + } + ] + }); + } + _addCheck(check) { + return new _ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message) + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message) + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message) + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message) + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message) + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +}; +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params) + }); +}; +var ZodBoolean = class extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params) + }); +}; +var ZodDate = class _ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx2.parsedType + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_date + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = void 0; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date" + }); + status.dirty(); + } + } else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date" + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()) + }; + } + _addCheck(check) { + return new _ZodDate({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message) + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message) + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +}; +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params) + }); +}; +var ZodSymbol = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params) + }); +}; +var ZodUndefined = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params) + }); +}; +var ZodNull = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params) + }); +}; +var ZodAny = class extends ZodType { + constructor() { + super(...arguments); + this._any = true; + } + _parse(input) { + return OK(input.data); + } +}; +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params) + }); +}; +var ZodUnknown = class extends ZodType { + constructor() { + super(...arguments); + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +}; +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params) + }); +}; +var ZodNever = class extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType + }); + return INVALID; + } +}; +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params) + }); +}; +var ZodVoid = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params) + }); +}; +var ZodArray = class _ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: tooSmall ? def.exactLength.value : void 0, + maximum: tooBig ? def.exactLength.value : void 0, + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result2) => { + return ParseStatus.mergeArray(status, result2); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new _ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) } + }); + } + max(maxLength, message) { + return new _ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) } + }); + } + length(len, message) { + return new _ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) } + }); + } + nonempty(message) { + return this.min(1, message); + } +}; +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params) + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape + }); + } else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element) + }); + } else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } else { + return schema; + } +} +var ZodObject = class _ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + this.nonstrict = this.passthrough; + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util2.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx2.parsedType + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] } + }); + } + } else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys + }); + status.dirty(); + } + } else if (unknownKeys === "strip") { + } else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } else { + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse( + new ParseInputLazyPath(ctx, value, ctx.path, key) + //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data + }); + } + } + if (ctx.common.async) { + return Promise.resolve().then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet + }); + } + return syncPairs; + }).then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new _ZodObject({ + ...this._def, + unknownKeys: "strict", + ...message !== void 0 ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError + }; + return { + message: defaultError + }; + } + } : {} + }); + } + strip() { + return new _ZodObject({ + ...this._def, + unknownKeys: "strip" + }); + } + passthrough() { + return new _ZodObject({ + ...this._def, + unknownKeys: "passthrough" + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new _ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation + }) + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new _ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape() + }), + typeName: ZodFirstPartyTypeKind.ZodObject + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new _ZodObject({ + ...this._def, + catchall: index + }); + } + pick(mask) { + const shape = {}; + for (const key of util2.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new _ZodObject({ + ...this._def, + shape: () => shape + }); + } + omit(mask) { + const shape = {}; + for (const key of util2.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new _ZodObject({ + ...this._def, + shape: () => shape + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util2.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } else { + newShape[key] = fieldSchema.optional(); + } + } + return new _ZodObject({ + ...this._def, + shape: () => newShape + }); + } + required(mask) { + const newShape = {}; + for (const key of util2.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new _ZodObject({ + ...this._def, + shape: () => newShape + }); + } + keyof() { + return createZodEnum(util2.objectKeys(this.shape)); + } +}; +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +var ZodUnion = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + }, + parent: null + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx + }), + ctx: childCtx + }; + })).then(handleResults); + } else { + let dirty = void 0; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + }, + parent: null + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx + }); + if (result.status === "valid") { + return result; + } else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues2) => new ZodError(issues2)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +}; +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params) + }); +}; +var getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } else if (type instanceof ZodLiteral) { + return [type.value]; + } else if (type instanceof ZodEnum) { + return type.options; + } else if (type instanceof ZodNativeEnum) { + return util2.objectValues(type.enum); + } else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } else if (type instanceof ZodUndefined) { + return [void 0]; + } else if (type instanceof ZodNull) { + return [null]; + } else if (type instanceof ZodOptional) { + return [void 0, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } else { + return []; + } +}; +var ZodDiscriminatedUnion = class _ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator] + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + } else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + const optionsMap = /* @__PURE__ */ new Map(); + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new _ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params) + }); + } +}; +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util2.objectKeys(b); + const sharedKeys = util2.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } else { + return { valid: false }; + } +} +var ZodIntersection = class extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }) + ]).then(([left, right]) => handleParsed(left, right)); + } else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + })); + } + } +}; +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left, + right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params) + }); +}; +var ZodTuple = class _ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array" + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array" + }); + status.dirty(); + } + const items = [...ctx.data].map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }).filter((x2) => !!x2); + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new _ZodTuple({ + ...this._def, + rest + }); + } +}; +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params) + }); +}; +var ZodRecord = class _ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new _ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third) + }); + } + return new _ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second) + }); + } +}; +var ZodMap = class extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])) + }; + }); + if (ctx.common.async) { + const finalMap = /* @__PURE__ */ new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } else { + const finalMap = /* @__PURE__ */ new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +}; +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params) + }); +}; +var ZodSet = class _ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements2) { + const parsedSet = /* @__PURE__ */ new Set(); + for (const element of elements2) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements2) => finalizeSet(elements2)); + } else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new _ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) } + }); + } + max(maxSize, message) { + return new _ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) } + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +}; +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params) + }); +}; +var ZodFunction = class _ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error + } + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error + } + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + const me2 = this; + return OK(async function(...args) { + const error = new ZodError([]); + const parsedArgs = await me2._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me2._def.returns._def.type.parseAsync(result, params).catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } else { + const me2 = this; + return OK(function(...args) { + const parsedArgs = me2._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me2._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new _ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()) + }); + } + returns(returnType) { + return new _ZodFunction({ + ...this._def, + returns: returnType + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new _ZodFunction({ + args: args ? args : ZodTuple.create([]).rest(ZodUnknown.create()), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params) + }); + } +}; +var ZodLazy = class extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +}; +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params) + }); +}; +var ZodLiteral = class extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +}; +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params) + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params) + }); +} +var ZodEnum = class _ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util2.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return _ZodEnum.create(values, { + ...this._def, + ...newDef + }); + } + exclude(values, newDef = this._def) { + return _ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef + }); + } +}; +ZodEnum.create = createZodEnum; +var ZodNativeEnum = class extends ZodType { + _parse(input) { + const nativeEnumValues = util2.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util2.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util2.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util2.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util2.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +}; +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params) + }); +}; +var ZodPromise = class extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap + }); + })); + } +}; +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params) + }); +}; +var ZodEffects = class extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects ? this._def.schema.sourceType() : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + } + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed2) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed2, + path: ctx.path, + parent: ctx + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result + })); + }); + } + } + util2.assertNever(effect); + } +}; +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params) + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params) + }); +}; +var ZodOptional = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(void 0); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +}; +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params) + }); +}; +var ZodNullable = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +}; +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params) + }); +}; +var ZodDefault = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx + }); + } + removeDefault() { + return this._def.innerType; + } +}; +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params) + }); +}; +var ZodCatch = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + } + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx + } + }); + if (isAsync(result)) { + return result.then((result2) => { + return { + status: "valid", + value: result2.status === "valid" ? result2.value : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data + }) + }; + }); + } else { + return { + status: "valid", + value: result.status === "valid" ? result.value : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data + }) + }; + } + } + removeCatch() { + return this._def.innerType; + } +}; +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params) + }); +}; +var ZodNaN = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +}; +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params) + }); +}; +var BRAND = /* @__PURE__ */ Symbol("zod_brand"); +var ZodBranded = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx + }); + } + unwrap() { + return this._def.type; + } +}; +var ZodPipeline = class _ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx + }); + } + }; + return handleAsync(); + } else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value + }; + } else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx + }); + } + } + } + static create(a, b) { + return new _ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline + }); + } +}; +var ZodReadonly = class extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +}; +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params) + }); +}; +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +function custom(check, _params = {}, fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r2) => { + if (!r2) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +var late = { + object: ZodObject.lazycreate +}; +var ZodFirstPartyTypeKind; +(function(ZodFirstPartyTypeKind2) { + ZodFirstPartyTypeKind2["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +var instanceOfType = (cls, params = { + message: `Input not instance of ${cls.name}` +}) => custom((data) => data instanceof cls, params); +var stringType = ZodString.create; +var numberType = ZodNumber.create; +var nanType = ZodNaN.create; +var bigIntType = ZodBigInt.create; +var booleanType = ZodBoolean.create; +var dateType = ZodDate.create; +var symbolType = ZodSymbol.create; +var undefinedType = ZodUndefined.create; +var nullType = ZodNull.create; +var anyType = ZodAny.create; +var unknownType = ZodUnknown.create; +var neverType = ZodNever.create; +var voidType = ZodVoid.create; +var arrayType = ZodArray.create; +var objectType = ZodObject.create; +var strictObjectType = ZodObject.strictCreate; +var unionType = ZodUnion.create; +var discriminatedUnionType = ZodDiscriminatedUnion.create; +var intersectionType = ZodIntersection.create; +var tupleType = ZodTuple.create; +var recordType = ZodRecord.create; +var mapType = ZodMap.create; +var setType = ZodSet.create; +var functionType = ZodFunction.create; +var lazyType = ZodLazy.create; +var literalType = ZodLiteral.create; +var enumType = ZodEnum.create; +var nativeEnumType = ZodNativeEnum.create; +var promiseType = ZodPromise.create; +var effectsType = ZodEffects.create; +var optionalType = ZodOptional.create; +var nullableType = ZodNullable.create; +var preprocessType = ZodEffects.createWithPreprocess; +var pipelineType = ZodPipeline.create; +var ostring = () => stringType().optional(); +var onumber = () => numberType().optional(); +var oboolean = () => booleanType().optional(); +var coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })) +}; +var NEVER = INVALID; + +// ../plugins/dist/utils.js +var isHookTree = (value) => { + if (value == null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + for (const child of Object.values(value)) { + if (child === void 0) { + continue; + } + if (typeof child === "function") { + continue; + } + if (child == null || typeof child !== "object" || Array.isArray(child) || !isHookTree(child)) { + return false; + } + } + return true; +}; + +// ../plugins/dist/plugin.js +var isHarnessPlugin = (value) => { + if (value == null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const candidate = value; + if (typeof candidate.name !== "string" || candidate.name.length === 0) { + return false; + } + if (candidate.createState != null && typeof candidate.createState !== "function") { + return false; + } + if (candidate.hooks != null && !isHookTree(candidate.hooks)) { + return false; + } + return true; +}; + +// ../plugins/dist/manager.js +var pluginsLogger = logger.child("plugins"); + +// ../config/dist/types.js +var DEFAULT_METRO_PORT = 8081; +var configLogger = logger.child("config"); +var RunnerSchema = external_exports.object({ + name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), + config: external_exports.record(external_exports.any()), + runner: external_exports.string(), + cli: external_exports.string().optional(), + platformId: external_exports.string() +}); +var PluginSchema = external_exports.custom((value) => isHarnessPlugin(value), "Invalid Harness plugin"); +var ConfigSchema = external_exports.object({ + entryPoint: external_exports.string().min(1, "Entry point is required"), + appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"), + runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"), + plugins: external_exports.array(PluginSchema).optional().default([]), + defaultRunner: external_exports.string().optional(), + host: external_exports.string().min(1, "Host is required").optional(), + metroPort: external_exports.number().int("Metro port must be an integer").min(1, "Metro port must be at least 1").max(65535, "Metro port must be at most 65535").optional().default(DEFAULT_METRO_PORT), + webSocketPort: external_exports.number().optional().describe("Deprecated. Bridge traffic now uses metroPort and this value is ignored."), + bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4), + testTimeout: external_exports.number().min(1e3, "Test timeout must be at least 1 second").default(5e3), + platformReadyTimeout: external_exports.number().min(1e3, "Platform ready timeout must be at least 1 second").default(3e5), + bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(6e4), + maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2), + eagerPrewarm: external_exports.boolean().optional().default(true).describe("Start building the Metro bundle while the platform (emulator, simulator, or browser) is still booting, so the first bundle is ready sooner. Disable to defer the first bundle build until app startup."), + resetEnvironmentBetweenTestFiles: external_exports.union([external_exports.boolean(), external_exports.enum(["process", "runtime"])]).optional().default(true).describe("Controls how the environment is reset between test files. `true` (default) and `'process'` kill and cold-restart the app process. `'runtime'` reloads the JS runtime in place (DevSettings.reload() / window.location.reload()), which is cheaper but escalates to a process restart if the reload fails or the app does not reconnect in time. `false` disables resetting the environment between test files entirely."), + skipAlreadyIncludedModules: external_exports.boolean().optional().describe('Skip re-sending modules already served in the main app bundle when Metro serves a per-test-file bundle. Defaults to true; set to false as an escape hatch if it causes issues. Left undefined (rather than defaulted here) so `resolveSkipAlreadyIncludedModules` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__skipAlreadyIncludedModules` alias.'), + // Deprecated alias for `skipAlreadyIncludedModules` -- see + // `resolveSkipAlreadyIncludedModules`. Left without a `.default()` for the + // same reason: a default value would be indistinguishable from the user + // never having set it, which would break alias-vs-explicit-flag precedence. + unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().describe("Deprecated. Use `skipAlreadyIncludedModules` instead."), + cache: external_exports.object({ + metro: external_exports.boolean().optional().describe('Enable persistent Metro transform and file-map caching under the .harness cache directory. Defaults to true; set to false to always start with a cold Metro cache. Left undefined (rather than defaulted here) so `resolveMetroCacheEnabled` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__enableMetroCache` alias.'), + version: external_exports.string().optional().describe("User-controlled salt folded into the Metro cacheVersion. Bump it to force-invalidate previously cached transforms.") + }).optional(), + // Deprecated alias for `cache.metro` -- see `resolveMetroCacheEnabled`. + // Left without a `.default()` for the same reason as + // `unstable__skipAlreadyIncludedModules`: a default value would be + // indistinguishable from the user never having set it, which would break + // alias-vs-explicit-flag precedence. + unstable__enableMetroCache: external_exports.boolean().optional().describe("Deprecated. Use `cache.metro` instead."), + permissions: external_exports.boolean().optional().default(false).describe("Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent."), + detectNativeCrashes: external_exports.boolean().optional().default(true), + crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), + disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."), + coverage: external_exports.object({ + root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`), + native: external_exports.object({ + ios: external_exports.object({ + pods: external_exports.array(external_exports.string()).min(1, "At least one pod name is required").describe("Pod names to instrument for native code coverage. Coverage flags are injected at pod install time via a CocoaPods hook. After tests, profraw data is collected and converted to lcov format.") + }).optional() + }).optional().describe("Native code coverage configuration.") + }).optional(), + forwardClientLogs: external_exports.boolean().optional().default(false).describe("Enable forwarding of console.log, console.warn, console.error, and other console method calls from the React Native app during the active test run. When enabled, app console output is attached to the active test result's console output."), + diagnostics: external_exports.boolean().optional().default(false).describe("Enable diagnostics tracing for the harness session. Records spans for session setup, Metro bundling, bridge/client events, and per-file test runs, then writes a Chrome Trace Event JSON file and prints a summary after each run. Can also be enabled via the RN_HARNESS_DIAGNOSTICS environment variable."), + // Deprecated property - used for migration detection + include: external_exports.array(external_exports.string()).optional() +}).refine((config) => { + if (config.defaultRunner) { + return config.runners.some((runner) => runner.name === config.defaultRunner); + } + return true; +}, { + message: "Default runner must match one of the configured runner names", + path: ["defaultRunner"] +}); + +// ../config/dist/errors.js +var ConfigValidationError = class extends HarnessError { + filePath; + validationErrors; + constructor(filePath, validationErrors) { + const lines = [ + `Configuration validation failed in ${filePath}`, + "", + "The following issues were found:", + "" + ]; + validationErrors.forEach((error, index) => { + lines.push(` ${index + 1}. ${error}`); + }); + lines.push("", "Please fix these issues and try again.", "For more information, visit: https://react-native-harness.dev/docs/configuration"); + super(lines.join("\n")); + this.filePath = filePath; + this.validationErrors = validationErrors; + this.name = "ConfigValidationError"; + } +}; +var ConfigNotFoundError = class extends HarnessError { + searchPath; + constructor(searchPath) { + const lines = [ + "Configuration file not found", + "", + `Searched for configuration files in: ${searchPath}`, + "and all parent directories.", + "", + "React Native Harness looks for one of these files:", + " \u2022 rn-harness.config.js", + " \u2022 rn-harness.config.mjs", + " \u2022 rn-harness.config.cjs", + " \u2022 rn-harness.config.json", + "", + "For more information, visit: https://www.react-native-harness.dev/docs/getting-started/configuration" + ]; + super(lines.join("\n")); + this.searchPath = searchPath; + this.name = "ConfigNotFoundError"; + } +}; +var ConfigLoadError = class extends HarnessError { + filePath; + cause; + constructor(filePath, cause) { + const lines = [ + "Failed to load configuration file", + "", + `File: ${filePath}`, + "" + ]; + if (cause) { + lines.push("Error details:"); + lines.push(` ${cause.message}`); + lines.push(""); + } + lines.push("This could be due to:", " \u2022 Syntax errors in your configuration file", " \u2022 Missing dependencies or modules", " \u2022 Invalid file format or encoding", " \u2022 File permissions issues", "", "Troubleshooting steps:", " 1. Check the file syntax and format", " 2. Ensure all required dependencies are installed", " 3. Verify file permissions", " 4. Try creating a new configuration file", "", "For more help, visit: https://www.react-native-harness.dev/docs/getting-started/configuration"); + super(lines.join("\n")); + this.filePath = filePath; + this.name = "ConfigLoadError"; + this.cause = cause; + } +}; + +// ../config/dist/reader.js +var import_node_path10 = __toESM(require("path"), 1); +var import_node_fs10 = __toESM(require("fs"), 1); +var import_node_module2 = require("module"); +var import_meta = {}; +var extensions = [".js", ".mjs", ".cjs", ".json"]; +var importUp = async (dir, name) => { + const filePath = import_node_path10.default.join(dir, name); + for (const ext of extensions) { + const filePathWithExt = `${filePath}${ext}`; + if (import_node_fs10.default.existsSync(filePathWithExt)) { + let rawConfig; + try { + if (ext === ".mjs") { + rawConfig = await import(filePathWithExt).then((module2) => module2.default); + } else { + const require2 = (0, import_node_module2.createRequire)(import_meta.url); + rawConfig = require2(filePathWithExt); + } + } catch (error) { + throw new ConfigLoadError(filePathWithExt, error instanceof Error ? error : void 0); + } + try { + const config = ConfigSchema.parse(rawConfig); + return { config, filePathWithExt, configDir: dir }; + } catch (error) { + if (error instanceof ZodError) { + const validationErrors = error.errors.map((err) => { + const path12 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : ""; + return `${err.message}${path12}`; + }); + throw new ConfigValidationError(filePathWithExt, validationErrors); + } + throw error; + } + } + } + const parentDir = import_node_path10.default.dirname(dir); + if (parentDir === dir) { + throw new ConfigNotFoundError(dir); + } + return importUp(parentDir, name); +}; +var getConfig = async (dir) => { + const { config, configDir } = await importUp(dir, "rn-harness.config"); + return { + config, + projectRoot: configDir + }; +}; + +// src/shared/metro-cache-inputs.ts +var import_node_fs11 = __toESM(require("fs")); +var import_node_path11 = __toESM(require("path")); +var FALLBACK_BUNDLER_METRO_VERSION = "unknown"; +var resolveRepoRoot = (projectRoot) => { + let dir = projectRoot; + while (true) { + if (import_node_fs11.default.existsSync(import_node_path11.default.join(dir, ".git"))) { + return dir; + } + const parent = import_node_path11.default.dirname(dir); + if (parent === dir) { + return projectRoot; + } + dir = parent; + } +}; +var resolveBundlerMetroVersion = (projectRoot) => { + try { + const packageJsonPath = require.resolve( + "@react-native-harness/bundler-metro/package.json", + { paths: [projectRoot] } + ); + const packageJson = JSON.parse(import_node_fs11.default.readFileSync(packageJsonPath, "utf8")); + if (typeof packageJson.version !== "string") { + throw new Error('"version" field is missing or not a string'); + } + return packageJson.version; + } catch (error) { + console.warn( + `Failed to resolve @react-native-harness/bundler-metro's version from "${projectRoot}". Falling back to "${FALLBACK_BUNDLER_METRO_VERSION}".`, + error + ); + return FALLBACK_BUNDLER_METRO_VERSION; + } +}; +var resolveMetroStaticInputs = (options) => computeMetroStaticInputs({ + repoRoot: resolveRepoRoot(options.projectRoot), + bundlerMetroVersion: resolveBundlerMetroVersion(options.projectRoot), + salt: options.cacheVersionSalt +}); + +// src/shared/plan-save.ts +var parseSnapshot = (raw) => { + if (!raw) { + return {}; + } + try { + return JSON.parse(raw); + } catch (error) { + console.warn( + "Failed to parse the metroSnapshot input. Treating the pre-run cache state as empty.", + error + ); + return {}; + } +}; +var parseSavePolicyMode = (raw) => { + if (raw === "always" || raw === "never" || raw === "default-branch") { + return raw; + } + return "default-branch"; +}; +var run = async () => { + try { + const projectRootInput = process.env.INPUT_PROJECTROOT; + const projectRoot = projectRootInput ? import_node_path12.default.resolve(projectRootInput) : process.cwd(); + console.info(`Planning Metro cache save for: ${projectRoot}`); + const { config, projectRoot: resolvedProjectRoot } = await getConfig( + projectRoot + ); + const githubOutput = process.env.GITHUB_OUTPUT; + if (!githubOutput) { + throw new Error("GITHUB_OUTPUT environment variable is not set"); + } + const staticInputs = resolveMetroStaticInputs({ + projectRoot: resolvedProjectRoot, + cacheVersionSalt: config.cache?.version + }); + const before = parseSnapshot(process.env.INPUT_METRO_SNAPSHOT); + const policy = { + mode: parseSavePolicyMode(process.env.INPUT_CACHESAVEPOLICY), + isDefaultBranch: process.env.IS_DEFAULT_BRANCH === "true" + }; + const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); + const os = process.env.RUNNER_OS ?? process.platform; + const savePlan = cache.planSave(before, policy, { + metro: { os, staticInputs } + }); + cache.writeKeysFile(savePlan); + const metroPlan = savePlan.domains.metro; + const output = `metroShouldSave=${metroPlan?.shouldSave ? "true" : "false"} +metroSaveKey=${metroPlan?.saveKey ?? ""} +`; + import_node_fs12.default.appendFileSync(githubOutput, output); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } else { + console.error("Failed to plan Metro cache save"); + } + process.exit(1); + } +}; +run(); diff --git a/actions/shared/snapshot-metro.cjs b/actions/shared/snapshot-metro.cjs new file mode 100644 index 00000000..aa462732 --- /dev/null +++ b/actions/shared/snapshot-metro.cjs @@ -0,0 +1,4881 @@ +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + +// ../../node_modules/picocolors/picocolors.js +var require_picocolors = __commonJS({ + "../../node_modules/picocolors/picocolors.js"(exports2, module2) { + "use strict"; + var p = process || {}; + var argv = p.argv || []; + var env = p.env || {}; + var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI); + var formatter = (open, close, replace = open) => (input) => { + let string = "" + input, index = string.indexOf(close, open.length); + return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close; + }; + var replaceClose = (string, close, replace, index) => { + let result = "", cursor = 0; + do { + result += string.substring(cursor, index) + replace; + cursor = index + close.length; + index = string.indexOf(close, cursor); + } while (~index); + return result + string.substring(cursor); + }; + var createColors = (enabled = isColorSupported) => { + let f = enabled ? formatter : () => String; + return { + isColorSupported: enabled, + reset: f("\x1B[0m", "\x1B[0m"), + bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"), + dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"), + italic: f("\x1B[3m", "\x1B[23m"), + underline: f("\x1B[4m", "\x1B[24m"), + inverse: f("\x1B[7m", "\x1B[27m"), + hidden: f("\x1B[8m", "\x1B[28m"), + strikethrough: f("\x1B[9m", "\x1B[29m"), + black: f("\x1B[30m", "\x1B[39m"), + red: f("\x1B[31m", "\x1B[39m"), + green: f("\x1B[32m", "\x1B[39m"), + yellow: f("\x1B[33m", "\x1B[39m"), + blue: f("\x1B[34m", "\x1B[39m"), + magenta: f("\x1B[35m", "\x1B[39m"), + cyan: f("\x1B[36m", "\x1B[39m"), + white: f("\x1B[37m", "\x1B[39m"), + gray: f("\x1B[90m", "\x1B[39m"), + bgBlack: f("\x1B[40m", "\x1B[49m"), + bgRed: f("\x1B[41m", "\x1B[49m"), + bgGreen: f("\x1B[42m", "\x1B[49m"), + bgYellow: f("\x1B[43m", "\x1B[49m"), + bgBlue: f("\x1B[44m", "\x1B[49m"), + bgMagenta: f("\x1B[45m", "\x1B[49m"), + bgCyan: f("\x1B[46m", "\x1B[49m"), + bgWhite: f("\x1B[47m", "\x1B[49m"), + blackBright: f("\x1B[90m", "\x1B[39m"), + redBright: f("\x1B[91m", "\x1B[39m"), + greenBright: f("\x1B[92m", "\x1B[39m"), + yellowBright: f("\x1B[93m", "\x1B[39m"), + blueBright: f("\x1B[94m", "\x1B[39m"), + magentaBright: f("\x1B[95m", "\x1B[39m"), + cyanBright: f("\x1B[96m", "\x1B[39m"), + whiteBright: f("\x1B[97m", "\x1B[39m"), + bgBlackBright: f("\x1B[100m", "\x1B[49m"), + bgRedBright: f("\x1B[101m", "\x1B[49m"), + bgGreenBright: f("\x1B[102m", "\x1B[49m"), + bgYellowBright: f("\x1B[103m", "\x1B[49m"), + bgBlueBright: f("\x1B[104m", "\x1B[49m"), + bgMagentaBright: f("\x1B[105m", "\x1B[49m"), + bgCyanBright: f("\x1B[106m", "\x1B[49m"), + bgWhiteBright: f("\x1B[107m", "\x1B[49m") + }; + }; + module2.exports = createColors(); + module2.exports.createColors = createColors; + } +}); + +// ../../node_modules/sisteransi/src/index.js +var require_src = __commonJS({ + "../../node_modules/sisteransi/src/index.js"(exports2, module2) { + "use strict"; + var ESC = "\x1B"; + var CSI = `${ESC}[`; + var beep = "\x07"; + var cursor = { + to(x2, y) { + if (!y) return `${CSI}${x2 + 1}G`; + return `${CSI}${y + 1};${x2 + 1}H`; + }, + move(x2, y) { + let ret = ""; + if (x2 < 0) ret += `${CSI}${-x2}D`; + else if (x2 > 0) ret += `${CSI}${x2}C`; + if (y < 0) ret += `${CSI}${-y}A`; + else if (y > 0) ret += `${CSI}${y}B`; + return ret; + }, + up: (count = 1) => `${CSI}${count}A`, + down: (count = 1) => `${CSI}${count}B`, + forward: (count = 1) => `${CSI}${count}C`, + backward: (count = 1) => `${CSI}${count}D`, + nextLine: (count = 1) => `${CSI}E`.repeat(count), + prevLine: (count = 1) => `${CSI}F`.repeat(count), + left: `${CSI}G`, + hide: `${CSI}?25l`, + show: `${CSI}?25h`, + save: `${ESC}7`, + restore: `${ESC}8` + }; + var scroll = { + up: (count = 1) => `${CSI}S`.repeat(count), + down: (count = 1) => `${CSI}T`.repeat(count) + }; + var erase = { + screen: `${CSI}2J`, + up: (count = 1) => `${CSI}1J`.repeat(count), + down: (count = 1) => `${CSI}J`.repeat(count), + line: `${CSI}2K`, + lineEnd: `${CSI}K`, + lineStart: `${CSI}1K`, + lines(count) { + let clear = ""; + for (let i = 0; i < count; i++) + clear += this.line + (i < count - 1 ? cursor.up() : ""); + if (count) + clear += cursor.left; + return clear; + } + }; + module2.exports = { cursor, scroll, erase, beep }; + } +}); + +// src/shared/snapshot-metro.ts +var import_node_fs11 = __toESM(require("fs")); +var import_node_path11 = __toESM(require("path")); + +// ../cache/dist/index.js +var import_node_fs9 = __toESM(require("fs"), 1); + +// ../tools/dist/net.js +var import_node_net = __toESM(require("net"), 1); + +// ../tools/dist/logger.js +var import_node_util = __toESM(require("util"), 1); +var import_picocolors = __toESM(require_picocolors(), 1); +var verbose = !!process.env.HARNESS_DEBUG; +var BASE_TAG = "[harness]"; +var INFO_TAG = import_picocolors.default.isColorSupported ? import_picocolors.default.reset(import_picocolors.default.inverse(import_picocolors.default.bold(import_picocolors.default.magenta(" HARNESS ")))) : "HARNESS"; +var ERROR_TAG = import_picocolors.default.isColorSupported ? import_picocolors.default.reset(import_picocolors.default.inverse(import_picocolors.default.bold(import_picocolors.default.red(" HARNESS ")))) : "HARNESS"; +var getTimestamp = () => (/* @__PURE__ */ new Date()).toISOString(); +var normalizeScope = (scope) => scope.trim().replace(/^\[+|\]+$/g, "").replace(/\]\[/g, "]["); +var formatPrefix = (scopes) => { + const suffix = scopes.map((scope) => `[${normalizeScope(scope)}]`).join(""); + return `${BASE_TAG}${suffix}`; +}; +var mapLines = (text, prefix) => text.split("\n").map((line) => `${prefix} ${line}`).join("\n"); +var writeLog = (level, scopes, messages) => { + if (!verbose && (level === "info" || level === "log" || level === "success")) { + const output2 = import_node_util.default.format(...messages); + const tag = INFO_TAG; + process.stderr.write(`${tag} ${output2} +`); + return; + } + if (!verbose && level === "error") { + const output2 = import_node_util.default.format(...messages); + process.stderr.write(`${ERROR_TAG} ${output2} +`); + return; + } + const method = level === "warn" ? console.warn : console.debug; + const output = import_node_util.default.format(...messages); + const prefix = `${getTimestamp()} ${formatPrefix(scopes)}`; + method(mapLines(output, prefix)); +}; +var setVerbose = (level) => { + verbose = level; +}; +var isVerbose = () => { + return verbose; +}; +var createScopedLogger = (scopes = []) => ({ + debug: (...messages) => { + if (!verbose) { + return; + } + writeLog("debug", scopes, messages); + }, + info: (...messages) => { + writeLog("info", scopes, messages); + }, + warn: (...messages) => { + writeLog("warn", scopes, messages); + }, + error: (...messages) => { + writeLog("error", scopes, messages); + }, + log: (...messages) => { + writeLog("log", scopes, messages); + }, + success: (...messages) => { + writeLog("success", scopes, messages); + }, + child: (scope) => createScopedLogger([...scopes, scope]), + setVerbose, + isVerbose +}); +var logger = createScopedLogger(); + +// ../../node_modules/@clack/core/dist/index.mjs +var import_node_process = require("process"); +var k = __toESM(require("readline"), 1); +var import_node_readline = __toESM(require("readline"), 1); +var import_sisteransi = __toESM(require_src(), 1); +var import_node_tty = require("tty"); +var Ft = { limit: 1 / 0, ellipsis: "" }; +var ft = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var j = "\x07"; +var Q = "["; +var dt = "]"; +var U = `${dt}8;;`; +var et = new RegExp(`(?:\\${Q}(?\\d+)m|\\${U}(?.*)${j})`, "y"); +var At = ["up", "down", "left", "right", "space", "enter", "cancel"]; +var _ = { actions: new Set(At), aliases: /* @__PURE__ */ new Map([["k", "up"], ["j", "down"], ["h", "left"], ["l", "right"], ["", "cancel"], ["escape", "cancel"]]), messages: { cancel: "Canceled", error: "Something went wrong" }, withGuide: true }; +var bt = globalThis.process.platform.startsWith("win"); + +// ../../node_modules/@clack/prompts/dist/index.mjs +var import_picocolors2 = __toESM(require_picocolors(), 1); +var import_node_process2 = __toESM(require("process"), 1); +var import_node_fs = require("fs"); +var import_node_path = require("path"); +var import_sisteransi2 = __toESM(require_src(), 1); +var import_node_util2 = require("util"); +function ht() { + return import_node_process2.default.platform !== "win32" ? import_node_process2.default.env.TERM !== "linux" : !!import_node_process2.default.env.CI || !!import_node_process2.default.env.WT_SESSION || !!import_node_process2.default.env.TERMINUS_SUBLIME || import_node_process2.default.env.ConEmuTask === "{cmd::Cmder}" || import_node_process2.default.env.TERM_PROGRAM === "Terminus-Sublime" || import_node_process2.default.env.TERM_PROGRAM === "vscode" || import_node_process2.default.env.TERM === "xterm-256color" || import_node_process2.default.env.TERM === "alacritty" || import_node_process2.default.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; +} +var ee = ht(); +var w = (e, r) => ee ? e : r; +var Me = w("\u25C6", "*"); +var ce = w("\u25A0", "x"); +var de = w("\u25B2", "x"); +var V = w("\u25C7", "o"); +var $e = w("\u250C", "T"); +var h = w("\u2502", "|"); +var x = w("\u2514", "\u2014"); +var Re = w("\u2510", "T"); +var Oe = w("\u2518", "\u2014"); +var Y = w("\u25CF", ">"); +var K = w("\u25CB", " "); +var te = w("\u25FB", "[\u2022]"); +var k2 = w("\u25FC", "[+]"); +var z = w("\u25FB", "[ ]"); +var Pe = w("\u25AA", "\u2022"); +var se = w("\u2500", "-"); +var he = w("\u256E", "+"); +var Ne = w("\u251C", "+"); +var me = w("\u256F", "+"); +var pe = w("\u2570", "+"); +var We = w("\u256D", "+"); +var ge = w("\u25CF", "\u2022"); +var fe = w("\u25C6", "*"); +var Fe = w("\u25B2", "!"); +var ye = w("\u25A0", "x"); +var Ft2 = { limit: 1 / 0, ellipsis: "" }; +var yt2 = { limit: 1 / 0, ellipsis: "", ellipsisWidth: 0 }; +var Ce = "\x07"; +var Ve = "["; +var vt = "]"; +var we = `${vt}8;;`; +var Ge = new RegExp(`(?:\\${Ve}(?\\d+)m|\\${we}(?.*)${Ce})`, "y"); +var Ut = import_picocolors2.default.magenta; +var Ye = { light: w("\u2500", "-"), heavy: w("\u2501", "="), block: w("\u2588", "#") }; +var ze = `${import_picocolors2.default.gray(h)} `; + +// ../tools/dist/spawn.js +var spawnLogger = logger.child("spawn"); + +// ../tools/dist/react-native.js +var import_node_module = require("module"); +var import_node_path2 = __toESM(require("path"), 1); +var import_node_fs2 = __toESM(require("fs"), 1); + +// ../tools/dist/error.js +var HarnessError = class extends Error { +}; + +// ../tools/dist/packages.js +var import_node_path3 = __toESM(require("path"), 1); +var import_node_fs3 = __toESM(require("fs"), 1); + +// ../tools/dist/path.js +var import_node_path4 = __toESM(require("path"), 1); + +// ../tools/dist/crash-artifacts.js +var import_node_fs4 = __toESM(require("fs"), 1); +var import_node_path5 = __toESM(require("path"), 1); +var DEFAULT_ARTIFACT_ROOT = import_node_path5.default.join(process.cwd(), ".harness", "crash-reports"); + +// ../tools/dist/harness-artifacts.js +var import_node_fs5 = __toESM(require("fs"), 1); +var import_node_path6 = __toESM(require("path"), 1); + +// ../tools/dist/diagnostics.js +var noop = () => void 0; +var noopSpan = { + end: noop, + fail: noop +}; +var createNoopDiagnostics = () => { + const diagnostics = { + enabled: false, + start: () => noopSpan, + measure: async (_label, fn) => fn(), + mark: noop, + record: noop, + child: () => diagnostics, + flush: () => [] + }; + return diagnostics; +}; +var noopDiagnostics = createNoopDiagnostics(); + +// ../cache/dist/paths.js +var import_node_path7 = __toESM(require("path"), 1); +var HARNESS_DIRNAME = ".harness"; +var CACHE_DIRNAME = "cache"; +var METRO_TRANSFORM_DIRNAME = "metro"; +var METRO_FILE_MAP_DIRNAME = "metro-file-map"; +var createHarnessCachePaths = (projectRoot) => { + const root = import_node_path7.default.join(projectRoot, HARNESS_DIRNAME, CACHE_DIRNAME); + return { + root, + metroTransform: import_node_path7.default.join(root, METRO_TRANSFORM_DIRNAME), + metroFileMap: import_node_path7.default.join(root, METRO_FILE_MAP_DIRNAME) + }; +}; +var getDomainDirectories = (domain, paths) => { + switch (domain) { + case "metro": + return [paths.metroTransform, paths.metroFileMap]; + default: + return []; + } +}; + +// ../cache/dist/plan.js +var import_node_crypto = require("crypto"); +var import_node_fs6 = __toESM(require("fs"), 1); +var import_node_path8 = __toESM(require("path"), 1); +var cacheLogger = logger.child("cache"); +var KEYS_FILENAME = "keys.json"; +var HASH_LENGTH = 16; +var ALL_DOMAINS = ["metro"]; +var isAccretiveDomain = (domain) => { + switch (domain) { + case "metro": + return true; + default: + return true; + } +}; +var hashSortedEntries = (entries) => (0, import_node_crypto.createHash)("sha256").update(entries.slice().sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("\n")).digest("hex").slice(0, HASH_LENGTH); +var buildStaticHashKey = (domain, inputs) => { + const hash = hashSortedEntries(Object.entries(inputs.staticInputs)); + return `harness-${domain}-${inputs.os}-${hash}`; +}; +var buildContentHash = (entries) => hashSortedEntries(entries.map((entry) => [entry.path, String(entry.sizeBytes)])); +var walkDirectory = (root, directory, entries) => { + let dirents; + try { + dirents = import_node_fs6.default.readdirSync(directory, { withFileTypes: true }); + } catch { + return; + } + for (const dirent of dirents) { + const fullPath = import_node_path8.default.join(directory, dirent.name); + if (dirent.isDirectory()) { + walkDirectory(root, fullPath, entries); + } else if (dirent.isFile()) { + const stat = import_node_fs6.default.statSync(fullPath); + entries.push({ + path: import_node_path8.default.relative(root, fullPath), + sizeBytes: stat.size, + mtimeMs: stat.mtimeMs + }); + } + } +}; +var planRestore = (paths, inputs) => { + const domains = {}; + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + const restoreKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + domains[domain] = { + paths: [...getDomainDirectories(domain, paths)], + restoreKey, + restorePrefixes: accretive ? [`${restoreKey}-`] : [], + accretive + }; + } + return { version: 1, domains }; +}; +var snapshot = (paths) => { + const result = {}; + for (const domain of ALL_DOMAINS) { + try { + const entries = []; + for (const directory of getDomainDirectories(domain, paths)) { + walkDirectory(paths.root, directory, entries); + } + result[domain] = entries; + } catch (error) { + cacheLogger.warn(`Failed to snapshot cache domain "${domain}". Treating as empty.`, error); + result[domain] = []; + } + } + return result; +}; +var diffSnapshots = (before, after) => { + const beforeByPath = new Map(before.map((entry) => [entry.path, entry])); + const afterByPath = new Map(after.map((entry) => [entry.path, entry])); + let addedEntries = 0; + let removedEntries = 0; + let addedBytes = 0; + for (const [entryPath, afterEntry] of afterByPath) { + const beforeEntry = beforeByPath.get(entryPath); + if (!beforeEntry || beforeEntry.sizeBytes !== afterEntry.sizeBytes) { + addedEntries += 1; + addedBytes += afterEntry.sizeBytes; + } + } + for (const entryPath of beforeByPath.keys()) { + if (!afterByPath.has(entryPath)) { + removedEntries += 1; + } + } + return { addedEntries, removedEntries, addedBytes }; +}; +var shouldSaveForPolicy = (policy, delta) => { + if (delta.addedEntries + delta.removedEntries === 0) { + return false; + } + switch (policy.mode) { + case "always": + return true; + case "default-branch": + return policy.isDefaultBranch; + case "never": + return false; + } +}; +var planSave = (paths, before, policy, inputs) => { + const after = snapshot(paths); + const domains = {}; + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + const beforeEntries = before[domain] ?? []; + const afterEntries = after[domain] ?? []; + const delta = diffSnapshots(beforeEntries, afterEntries); + const staticHashKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + domains[domain] = { + shouldSave: shouldSaveForPolicy(policy, delta), + saveKey: accretive ? `${staticHashKey}-${buildContentHash(afterEntries)}` : staticHashKey, + delta + }; + } + return { version: 1, domains }; +}; +var writeKeysFile = (paths, plan) => { + const filePath = import_node_path8.default.join(paths.root, KEYS_FILENAME); + try { + import_node_fs6.default.mkdirSync(paths.root, { recursive: true }); + import_node_fs6.default.writeFileSync(filePath, JSON.stringify(plan, null, 2)); + } catch (error) { + cacheLogger.warn(`Failed to write cache keys file "${filePath}".`, error); + } +}; + +// ../cache/dist/warmth.js +var import_node_fs7 = __toESM(require("fs"), 1); +var cacheLogger2 = logger.child("cache"); +var isDomainWarm = (domain, paths) => { + const directory = getWarmthDirectory(domain, paths); + if (!directory) { + return false; + } + try { + const stat = import_node_fs7.default.statSync(directory); + if (!stat.isDirectory()) { + return false; + } + return import_node_fs7.default.readdirSync(directory).length > 0; + } catch (error) { + if (isMissingPathError(error)) { + return false; + } + cacheLogger2.warn(`Failed to check cache warmth for "${domain}" at "${directory}". Treating as cold.`, error); + return false; + } +}; +var getWarmthDirectory = (domain, paths) => { + switch (domain) { + case "metro": + return paths.metroTransform; + default: + return void 0; + } +}; +var isMissingPathError = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"; + +// ../cache/dist/domains/metro.js +var import_node_crypto2 = require("crypto"); +var import_node_fs8 = __toESM(require("fs"), 1); +var import_node_path9 = __toESM(require("path"), 1); +var cacheLogger3 = logger.child("cache"); +var METRO_KEY_FILENAMES = [ + "bun.lock", + "bun.lockb", + "package-lock.json", + "npm-shrinkwrap.json", + "pnpm-lock.yaml", + "yarn.lock", + "metro.config.js", + "metro.config.cjs", + "metro.config.mjs", + "metro.config.ts", + "babel.config.js", + "babel.config.cjs", + "babel.config.mjs", + "babel.config.ts", + "babel.config.json" +]; +var METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); + +// ../cache/dist/index.js +var cacheLogger4 = logger.child("cache"); +var createHarnessCache = (options) => { + const paths = createHarnessCachePaths(options.projectRoot); + return { + paths, + isWarm: (domain) => isDomainWarm(domain, paths), + ensureDomainDirectories: (domain) => { + for (const directory of getDomainDirectories(domain, paths)) { + try { + import_node_fs9.default.mkdirSync(directory, { recursive: true }); + } catch (error) { + cacheLogger4.warn(`Failed to create cache directory "${directory}". Continuing with a cold cache.`, error); + } + } + }, + planRestore: (inputs) => planRestore(paths, inputs), + snapshot: () => snapshot(paths), + planSave: (before, policy, inputs) => planSave(paths, before, policy, inputs), + writeKeysFile: (plan) => writeKeysFile(paths, plan) + }; +}; + +// ../../node_modules/zod/dist/esm/v3/external.js +var external_exports = {}; +__export(external_exports, { + BRAND: () => BRAND, + DIRTY: () => DIRTY, + EMPTY_PATH: () => EMPTY_PATH, + INVALID: () => INVALID, + NEVER: () => NEVER, + OK: () => OK, + ParseStatus: () => ParseStatus, + Schema: () => ZodType, + ZodAny: () => ZodAny, + ZodArray: () => ZodArray, + ZodBigInt: () => ZodBigInt, + ZodBoolean: () => ZodBoolean, + ZodBranded: () => ZodBranded, + ZodCatch: () => ZodCatch, + ZodDate: () => ZodDate, + ZodDefault: () => ZodDefault, + ZodDiscriminatedUnion: () => ZodDiscriminatedUnion, + ZodEffects: () => ZodEffects, + ZodEnum: () => ZodEnum, + ZodError: () => ZodError, + ZodFirstPartyTypeKind: () => ZodFirstPartyTypeKind, + ZodFunction: () => ZodFunction, + ZodIntersection: () => ZodIntersection, + ZodIssueCode: () => ZodIssueCode, + ZodLazy: () => ZodLazy, + ZodLiteral: () => ZodLiteral, + ZodMap: () => ZodMap, + ZodNaN: () => ZodNaN, + ZodNativeEnum: () => ZodNativeEnum, + ZodNever: () => ZodNever, + ZodNull: () => ZodNull, + ZodNullable: () => ZodNullable, + ZodNumber: () => ZodNumber, + ZodObject: () => ZodObject, + ZodOptional: () => ZodOptional, + ZodParsedType: () => ZodParsedType, + ZodPipeline: () => ZodPipeline, + ZodPromise: () => ZodPromise, + ZodReadonly: () => ZodReadonly, + ZodRecord: () => ZodRecord, + ZodSchema: () => ZodType, + ZodSet: () => ZodSet, + ZodString: () => ZodString, + ZodSymbol: () => ZodSymbol, + ZodTransformer: () => ZodEffects, + ZodTuple: () => ZodTuple, + ZodType: () => ZodType, + ZodUndefined: () => ZodUndefined, + ZodUnion: () => ZodUnion, + ZodUnknown: () => ZodUnknown, + ZodVoid: () => ZodVoid, + addIssueToContext: () => addIssueToContext, + any: () => anyType, + array: () => arrayType, + bigint: () => bigIntType, + boolean: () => booleanType, + coerce: () => coerce, + custom: () => custom, + date: () => dateType, + datetimeRegex: () => datetimeRegex, + defaultErrorMap: () => en_default, + discriminatedUnion: () => discriminatedUnionType, + effect: () => effectsType, + enum: () => enumType, + function: () => functionType, + getErrorMap: () => getErrorMap, + getParsedType: () => getParsedType, + instanceof: () => instanceOfType, + intersection: () => intersectionType, + isAborted: () => isAborted, + isAsync: () => isAsync, + isDirty: () => isDirty, + isValid: () => isValid, + late: () => late, + lazy: () => lazyType, + literal: () => literalType, + makeIssue: () => makeIssue, + map: () => mapType, + nan: () => nanType, + nativeEnum: () => nativeEnumType, + never: () => neverType, + null: () => nullType, + nullable: () => nullableType, + number: () => numberType, + object: () => objectType, + objectUtil: () => objectUtil, + oboolean: () => oboolean, + onumber: () => onumber, + optional: () => optionalType, + ostring: () => ostring, + pipeline: () => pipelineType, + preprocess: () => preprocessType, + promise: () => promiseType, + quotelessJson: () => quotelessJson, + record: () => recordType, + set: () => setType, + setErrorMap: () => setErrorMap, + strictObject: () => strictObjectType, + string: () => stringType, + symbol: () => symbolType, + transformer: () => effectsType, + tuple: () => tupleType, + undefined: () => undefinedType, + union: () => unionType, + unknown: () => unknownType, + util: () => util2, + void: () => voidType +}); + +// ../../node_modules/zod/dist/esm/v3/helpers/util.js +var util2; +(function(util3) { + util3.assertEqual = (_2) => { + }; + function assertIs(_arg) { + } + util3.assertIs = assertIs; + function assertNever(_x) { + throw new Error(); + } + util3.assertNever = assertNever; + util3.arrayToEnum = (items) => { + const obj = {}; + for (const item of items) { + obj[item] = item; + } + return obj; + }; + util3.getValidEnumValues = (obj) => { + const validKeys = util3.objectKeys(obj).filter((k3) => typeof obj[obj[k3]] !== "number"); + const filtered = {}; + for (const k3 of validKeys) { + filtered[k3] = obj[k3]; + } + return util3.objectValues(filtered); + }; + util3.objectValues = (obj) => { + return util3.objectKeys(obj).map(function(e) { + return obj[e]; + }); + }; + util3.objectKeys = typeof Object.keys === "function" ? (obj) => Object.keys(obj) : (object) => { + const keys = []; + for (const key in object) { + if (Object.prototype.hasOwnProperty.call(object, key)) { + keys.push(key); + } + } + return keys; + }; + util3.find = (arr, checker) => { + for (const item of arr) { + if (checker(item)) + return item; + } + return void 0; + }; + util3.isInteger = typeof Number.isInteger === "function" ? (val) => Number.isInteger(val) : (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val; + function joinValues(array, separator = " | ") { + return array.map((val) => typeof val === "string" ? `'${val}'` : val).join(separator); + } + util3.joinValues = joinValues; + util3.jsonStringifyReplacer = (_2, value) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; +})(util2 || (util2 = {})); +var objectUtil; +(function(objectUtil2) { + objectUtil2.mergeShapes = (first, second) => { + return { + ...first, + ...second + // second overwrites first + }; + }; +})(objectUtil || (objectUtil = {})); +var ZodParsedType = util2.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set" +]); +var getParsedType = (data) => { + const t2 = typeof data; + switch (t2) { + case "undefined": + return ZodParsedType.undefined; + case "string": + return ZodParsedType.string; + case "number": + return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number; + case "boolean": + return ZodParsedType.boolean; + case "function": + return ZodParsedType.function; + case "bigint": + return ZodParsedType.bigint; + case "symbol": + return ZodParsedType.symbol; + case "object": + if (Array.isArray(data)) { + return ZodParsedType.array; + } + if (data === null) { + return ZodParsedType.null; + } + if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") { + return ZodParsedType.promise; + } + if (typeof Map !== "undefined" && data instanceof Map) { + return ZodParsedType.map; + } + if (typeof Set !== "undefined" && data instanceof Set) { + return ZodParsedType.set; + } + if (typeof Date !== "undefined" && data instanceof Date) { + return ZodParsedType.date; + } + return ZodParsedType.object; + default: + return ZodParsedType.unknown; + } +}; + +// ../../node_modules/zod/dist/esm/v3/ZodError.js +var ZodIssueCode = util2.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite" +]); +var quotelessJson = (obj) => { + const json = JSON.stringify(obj, null, 2); + return json.replace(/"([^"]+)":/g, "$1:"); +}; +var ZodError = class _ZodError extends Error { + get errors() { + return this.issues; + } + constructor(issues) { + super(); + this.issues = []; + this.addIssue = (sub) => { + this.issues = [...this.issues, sub]; + }; + this.addIssues = (subs = []) => { + this.issues = [...this.issues, ...subs]; + }; + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } else { + this.__proto__ = actualProto; + } + this.name = "ZodError"; + this.issues = issues; + } + format(_mapper) { + const mapper = _mapper || function(issue) { + return issue.message; + }; + const fieldErrors = { _errors: [] }; + const processError = (error) => { + for (const issue of error.issues) { + if (issue.code === "invalid_union") { + issue.unionErrors.map(processError); + } else if (issue.code === "invalid_return_type") { + processError(issue.returnTypeError); + } else if (issue.code === "invalid_arguments") { + processError(issue.argumentsError); + } else if (issue.path.length === 0) { + fieldErrors._errors.push(mapper(issue)); + } else { + let curr = fieldErrors; + let i = 0; + while (i < issue.path.length) { + const el = issue.path[i]; + const terminal = i === issue.path.length - 1; + if (!terminal) { + curr[el] = curr[el] || { _errors: [] }; + } else { + curr[el] = curr[el] || { _errors: [] }; + curr[el]._errors.push(mapper(issue)); + } + curr = curr[el]; + i++; + } + } + } + }; + processError(this); + return fieldErrors; + } + static assert(value) { + if (!(value instanceof _ZodError)) { + throw new Error(`Not a ZodError: ${value}`); + } + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, util2.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(mapper = (issue) => issue.message) { + const fieldErrors = {}; + const formErrors = []; + for (const sub of this.issues) { + if (sub.path.length > 0) { + fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || []; + fieldErrors[sub.path[0]].push(mapper(sub)); + } else { + formErrors.push(mapper(sub)); + } + } + return { formErrors, fieldErrors }; + } + get formErrors() { + return this.flatten(); + } +}; +ZodError.create = (issues) => { + const error = new ZodError(issues); + return error; +}; + +// ../../node_modules/zod/dist/esm/v3/locales/en.js +var errorMap = (issue, _ctx) => { + let message; + switch (issue.code) { + case ZodIssueCode.invalid_type: + if (issue.received === ZodParsedType.undefined) { + message = "Required"; + } else { + message = `Expected ${issue.expected}, received ${issue.received}`; + } + break; + case ZodIssueCode.invalid_literal: + message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util2.jsonStringifyReplacer)}`; + break; + case ZodIssueCode.unrecognized_keys: + message = `Unrecognized key(s) in object: ${util2.joinValues(issue.keys, ", ")}`; + break; + case ZodIssueCode.invalid_union: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_union_discriminator: + message = `Invalid discriminator value. Expected ${util2.joinValues(issue.options)}`; + break; + case ZodIssueCode.invalid_enum_value: + message = `Invalid enum value. Expected ${util2.joinValues(issue.options)}, received '${issue.received}'`; + break; + case ZodIssueCode.invalid_arguments: + message = `Invalid function arguments`; + break; + case ZodIssueCode.invalid_return_type: + message = `Invalid function return type`; + break; + case ZodIssueCode.invalid_date: + message = `Invalid date`; + break; + case ZodIssueCode.invalid_string: + if (typeof issue.validation === "object") { + if ("includes" in issue.validation) { + message = `Invalid input: must include "${issue.validation.includes}"`; + if (typeof issue.validation.position === "number") { + message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`; + } + } else if ("startsWith" in issue.validation) { + message = `Invalid input: must start with "${issue.validation.startsWith}"`; + } else if ("endsWith" in issue.validation) { + message = `Invalid input: must end with "${issue.validation.endsWith}"`; + } else { + util2.assertNever(issue.validation); + } + } else if (issue.validation !== "regex") { + message = `Invalid ${issue.validation}`; + } else { + message = "Invalid"; + } + break; + case ZodIssueCode.too_small: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.too_big: + if (issue.type === "array") + message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`; + else if (issue.type === "string") + message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`; + else if (issue.type === "number") + message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "bigint") + message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`; + else if (issue.type === "date") + message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`; + else + message = "Invalid input"; + break; + case ZodIssueCode.custom: + message = `Invalid input`; + break; + case ZodIssueCode.invalid_intersection_types: + message = `Intersection results could not be merged`; + break; + case ZodIssueCode.not_multiple_of: + message = `Number must be a multiple of ${issue.multipleOf}`; + break; + case ZodIssueCode.not_finite: + message = "Number must be finite"; + break; + default: + message = _ctx.defaultError; + util2.assertNever(issue); + } + return { message }; +}; +var en_default = errorMap; + +// ../../node_modules/zod/dist/esm/v3/errors.js +var overrideErrorMap = en_default; +function setErrorMap(map) { + overrideErrorMap = map; +} +function getErrorMap() { + return overrideErrorMap; +} + +// ../../node_modules/zod/dist/esm/v3/helpers/parseUtil.js +var makeIssue = (params) => { + const { data, path: path11, errorMaps, issueData } = params; + const fullPath = [...path11, ...issueData.path || []]; + const fullIssue = { + ...issueData, + path: fullPath + }; + if (issueData.message !== void 0) { + return { + ...issueData, + path: fullPath, + message: issueData.message + }; + } + let errorMessage = ""; + const maps = errorMaps.filter((m) => !!m).slice().reverse(); + for (const map of maps) { + errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message; + } + return { + ...issueData, + path: fullPath, + message: errorMessage + }; +}; +var EMPTY_PATH = []; +function addIssueToContext(ctx, issueData) { + const overrideMap = getErrorMap(); + const issue = makeIssue({ + issueData, + data: ctx.data, + path: ctx.path, + errorMaps: [ + ctx.common.contextualErrorMap, + // contextual error map is first priority + ctx.schemaErrorMap, + // then schema-bound map if available + overrideMap, + // then global override map + overrideMap === en_default ? void 0 : en_default + // then global default map + ].filter((x2) => !!x2) + }); + ctx.common.issues.push(issue); +} +var ParseStatus = class _ParseStatus { + constructor() { + this.value = "valid"; + } + dirty() { + if (this.value === "valid") + this.value = "dirty"; + } + abort() { + if (this.value !== "aborted") + this.value = "aborted"; + } + static mergeArray(status, results) { + const arrayValue = []; + for (const s of results) { + if (s.status === "aborted") + return INVALID; + if (s.status === "dirty") + status.dirty(); + arrayValue.push(s.value); + } + return { status: status.value, value: arrayValue }; + } + static async mergeObjectAsync(status, pairs) { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value + }); + } + return _ParseStatus.mergeObjectSync(status, syncPairs); + } + static mergeObjectSync(status, pairs) { + const finalObject = {}; + for (const pair of pairs) { + const { key, value } = pair; + if (key.status === "aborted") + return INVALID; + if (value.status === "aborted") + return INVALID; + if (key.status === "dirty") + status.dirty(); + if (value.status === "dirty") + status.dirty(); + if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) { + finalObject[key.value] = value.value; + } + } + return { status: status.value, value: finalObject }; + } +}; +var INVALID = Object.freeze({ + status: "aborted" +}); +var DIRTY = (value) => ({ status: "dirty", value }); +var OK = (value) => ({ status: "valid", value }); +var isAborted = (x2) => x2.status === "aborted"; +var isDirty = (x2) => x2.status === "dirty"; +var isValid = (x2) => x2.status === "valid"; +var isAsync = (x2) => typeof Promise !== "undefined" && x2 instanceof Promise; + +// ../../node_modules/zod/dist/esm/v3/helpers/errorUtil.js +var errorUtil; +(function(errorUtil2) { + errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {}; + errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message; +})(errorUtil || (errorUtil = {})); + +// ../../node_modules/zod/dist/esm/v3/types.js +var ParseInputLazyPath = class { + constructor(parent, value, path11, key) { + this._cachedPath = []; + this.parent = parent; + this.data = value; + this._path = path11; + this._key = key; + } + get path() { + if (!this._cachedPath.length) { + if (Array.isArray(this._key)) { + this._cachedPath.push(...this._path, ...this._key); + } else { + this._cachedPath.push(...this._path, this._key); + } + } + return this._cachedPath; + } +}; +var handleResult = (ctx, result) => { + if (isValid(result)) { + return { success: true, data: result.value }; + } else { + if (!ctx.common.issues.length) { + throw new Error("Validation failed but no issues detected."); + } + return { + success: false, + get error() { + if (this._error) + return this._error; + const error = new ZodError(ctx.common.issues); + this._error = error; + return this._error; + } + }; + } +}; +function processCreateParams(params) { + if (!params) + return {}; + const { errorMap: errorMap2, invalid_type_error, required_error, description } = params; + if (errorMap2 && (invalid_type_error || required_error)) { + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + } + if (errorMap2) + return { errorMap: errorMap2, description }; + const customMap = (iss, ctx) => { + const { message } = params; + if (iss.code === "invalid_enum_value") { + return { message: message ?? ctx.defaultError }; + } + if (typeof ctx.data === "undefined") { + return { message: message ?? required_error ?? ctx.defaultError }; + } + if (iss.code !== "invalid_type") + return { message: ctx.defaultError }; + return { message: message ?? invalid_type_error ?? ctx.defaultError }; + }; + return { errorMap: customMap, description }; +} +var ZodType = class { + get description() { + return this._def.description; + } + _getType(input) { + return getParsedType(input.data); + } + _getOrReturnCtx(input, ctx) { + return ctx || { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent + }; + } + _processInputParams(input) { + return { + status: new ParseStatus(), + ctx: { + common: input.parent.common, + data: input.data, + parsedType: getParsedType(input.data), + schemaErrorMap: this._def.errorMap, + path: input.path, + parent: input.parent + } + }; + } + _parseSync(input) { + const result = this._parse(input); + if (isAsync(result)) { + throw new Error("Synchronous parse encountered promise."); + } + return result; + } + _parseAsync(input) { + const result = this._parse(input); + return Promise.resolve(result); + } + parse(data, params) { + const result = this.safeParse(data, params); + if (result.success) + return result.data; + throw result.error; + } + safeParse(data, params) { + const ctx = { + common: { + issues: [], + async: params?.async ?? false, + contextualErrorMap: params?.errorMap + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + const result = this._parseSync({ data, path: ctx.path, parent: ctx }); + return handleResult(ctx, result); + } + "~validate"(data) { + const ctx = { + common: { + issues: [], + async: !!this["~standard"].async + }, + path: [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + if (!this["~standard"].async) { + try { + const result = this._parseSync({ data, path: [], parent: ctx }); + return isValid(result) ? { + value: result.value + } : { + issues: ctx.common.issues + }; + } catch (err) { + if (err?.message?.toLowerCase()?.includes("encountered")) { + this["~standard"].async = true; + } + ctx.common = { + issues: [], + async: true + }; + } + } + return this._parseAsync({ data, path: [], parent: ctx }).then((result) => isValid(result) ? { + value: result.value + } : { + issues: ctx.common.issues + }); + } + async parseAsync(data, params) { + const result = await this.safeParseAsync(data, params); + if (result.success) + return result.data; + throw result.error; + } + async safeParseAsync(data, params) { + const ctx = { + common: { + issues: [], + contextualErrorMap: params?.errorMap, + async: true + }, + path: params?.path || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data, + parsedType: getParsedType(data) + }; + const maybeAsyncResult = this._parse({ data, path: ctx.path, parent: ctx }); + const result = await (isAsync(maybeAsyncResult) ? maybeAsyncResult : Promise.resolve(maybeAsyncResult)); + return handleResult(ctx, result); + } + refine(check, message) { + const getIssueProperties = (val) => { + if (typeof message === "string" || typeof message === "undefined") { + return { message }; + } else if (typeof message === "function") { + return message(val); + } else { + return message; + } + }; + return this._refinement((val, ctx) => { + const result = check(val); + const setError = () => ctx.addIssue({ + code: ZodIssueCode.custom, + ...getIssueProperties(val) + }); + if (typeof Promise !== "undefined" && result instanceof Promise) { + return result.then((data) => { + if (!data) { + setError(); + return false; + } else { + return true; + } + }); + } + if (!result) { + setError(); + return false; + } else { + return true; + } + }); + } + refinement(check, refinementData) { + return this._refinement((val, ctx) => { + if (!check(val)) { + ctx.addIssue(typeof refinementData === "function" ? refinementData(val, ctx) : refinementData); + return false; + } else { + return true; + } + }); + } + _refinement(refinement) { + return new ZodEffects({ + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "refinement", refinement } + }); + } + superRefine(refinement) { + return this._refinement(refinement); + } + constructor(def) { + this.spa = this.safeParseAsync; + this._def = def; + this.parse = this.parse.bind(this); + this.safeParse = this.safeParse.bind(this); + this.parseAsync = this.parseAsync.bind(this); + this.safeParseAsync = this.safeParseAsync.bind(this); + this.spa = this.spa.bind(this); + this.refine = this.refine.bind(this); + this.refinement = this.refinement.bind(this); + this.superRefine = this.superRefine.bind(this); + this.optional = this.optional.bind(this); + this.nullable = this.nullable.bind(this); + this.nullish = this.nullish.bind(this); + this.array = this.array.bind(this); + this.promise = this.promise.bind(this); + this.or = this.or.bind(this); + this.and = this.and.bind(this); + this.transform = this.transform.bind(this); + this.brand = this.brand.bind(this); + this.default = this.default.bind(this); + this.catch = this.catch.bind(this); + this.describe = this.describe.bind(this); + this.pipe = this.pipe.bind(this); + this.readonly = this.readonly.bind(this); + this.isNullable = this.isNullable.bind(this); + this.isOptional = this.isOptional.bind(this); + this["~standard"] = { + version: 1, + vendor: "zod", + validate: (data) => this["~validate"](data) + }; + } + optional() { + return ZodOptional.create(this, this._def); + } + nullable() { + return ZodNullable.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return ZodArray.create(this); + } + promise() { + return ZodPromise.create(this, this._def); + } + or(option) { + return ZodUnion.create([this, option], this._def); + } + and(incoming) { + return ZodIntersection.create(this, incoming, this._def); + } + transform(transform) { + return new ZodEffects({ + ...processCreateParams(this._def), + schema: this, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect: { type: "transform", transform } + }); + } + default(def) { + const defaultValueFunc = typeof def === "function" ? def : () => def; + return new ZodDefault({ + ...processCreateParams(this._def), + innerType: this, + defaultValue: defaultValueFunc, + typeName: ZodFirstPartyTypeKind.ZodDefault + }); + } + brand() { + return new ZodBranded({ + typeName: ZodFirstPartyTypeKind.ZodBranded, + type: this, + ...processCreateParams(this._def) + }); + } + catch(def) { + const catchValueFunc = typeof def === "function" ? def : () => def; + return new ZodCatch({ + ...processCreateParams(this._def), + innerType: this, + catchValue: catchValueFunc, + typeName: ZodFirstPartyTypeKind.ZodCatch + }); + } + describe(description) { + const This = this.constructor; + return new This({ + ...this._def, + description + }); + } + pipe(target) { + return ZodPipeline.create(this, target); + } + readonly() { + return ZodReadonly.create(this); + } + isOptional() { + return this.safeParse(void 0).success; + } + isNullable() { + return this.safeParse(null).success; + } +}; +var cuidRegex = /^c[^\s-]{8,}$/i; +var cuid2Regex = /^[0-9a-z]+$/; +var ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; +var uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i; +var nanoidRegex = /^[a-z0-9_-]{21}$/i; +var jwtRegex = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$/; +var durationRegex = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/; +var emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i; +var _emojiRegex = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`; +var emojiRegex; +var ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/; +var ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/(3[0-2]|[12]?[0-9])$/; +var ipv6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; +var ipv6CidrRegex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/; +var base64Regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/; +var base64urlRegex = /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/; +var dateRegexSource = `((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))`; +var dateRegex = new RegExp(`^${dateRegexSource}$`); +function timeRegexSource(args) { + let secondsRegexSource = `[0-5]\\d`; + if (args.precision) { + secondsRegexSource = `${secondsRegexSource}\\.\\d{${args.precision}}`; + } else if (args.precision == null) { + secondsRegexSource = `${secondsRegexSource}(\\.\\d+)?`; + } + const secondsQuantifier = args.precision ? "+" : "?"; + return `([01]\\d|2[0-3]):[0-5]\\d(:${secondsRegexSource})${secondsQuantifier}`; +} +function timeRegex(args) { + return new RegExp(`^${timeRegexSource(args)}$`); +} +function datetimeRegex(args) { + let regex = `${dateRegexSource}T${timeRegexSource(args)}`; + const opts = []; + opts.push(args.local ? `Z?` : `Z`); + if (args.offset) + opts.push(`([+-]\\d{2}:?\\d{2})`); + regex = `${regex}(${opts.join("|")})`; + return new RegExp(`^${regex}$`); +} +function isValidIP(ip, version) { + if ((version === "v4" || !version) && ipv4Regex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6Regex.test(ip)) { + return true; + } + return false; +} +function isValidJWT(jwt, alg) { + if (!jwtRegex.test(jwt)) + return false; + try { + const [header] = jwt.split("."); + const base64 = header.replace(/-/g, "+").replace(/_/g, "/").padEnd(header.length + (4 - header.length % 4) % 4, "="); + const decoded = JSON.parse(atob(base64)); + if (typeof decoded !== "object" || decoded === null) + return false; + if ("typ" in decoded && decoded?.typ !== "JWT") + return false; + if (!decoded.alg) + return false; + if (alg && decoded.alg !== alg) + return false; + return true; + } catch { + return false; + } +} +function isValidCidr(ip, version) { + if ((version === "v4" || !version) && ipv4CidrRegex.test(ip)) { + return true; + } + if ((version === "v6" || !version) && ipv6CidrRegex.test(ip)) { + return true; + } + return false; +} +var ZodString = class _ZodString extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = String(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.string) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.string, + received: ctx2.parsedType + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = void 0; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.length < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + if (input.data.length > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "length") { + const tooBig = input.data.length > check.value; + const tooSmall = input.data.length < check.value; + if (tooBig || tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + if (tooBig) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message + }); + } else if (tooSmall) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "string", + inclusive: true, + exact: true, + message: check.message + }); + } + status.dirty(); + } + } else if (check.kind === "email") { + if (!emailRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "email", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "emoji") { + if (!emojiRegex) { + emojiRegex = new RegExp(_emojiRegex, "u"); + } + if (!emojiRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "emoji", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "uuid") { + if (!uuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "uuid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "nanoid") { + if (!nanoidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "nanoid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cuid") { + if (!cuidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cuid2") { + if (!cuid2Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cuid2", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "ulid") { + if (!ulidRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ulid", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "url") { + try { + new URL(input.data); + } catch { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "url", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "regex") { + check.regex.lastIndex = 0; + const testResult = check.regex.test(input.data); + if (!testResult) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "regex", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "trim") { + input.data = input.data.trim(); + } else if (check.kind === "includes") { + if (!input.data.includes(check.value, check.position)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { includes: check.value, position: check.position }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "toLowerCase") { + input.data = input.data.toLowerCase(); + } else if (check.kind === "toUpperCase") { + input.data = input.data.toUpperCase(); + } else if (check.kind === "startsWith") { + if (!input.data.startsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { startsWith: check.value }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "endsWith") { + if (!input.data.endsWith(check.value)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: { endsWith: check.value }, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "datetime") { + const regex = datetimeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "datetime", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "date") { + const regex = dateRegex; + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "date", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "time") { + const regex = timeRegex(check); + if (!regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_string, + validation: "time", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "duration") { + if (!durationRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "duration", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "ip") { + if (!isValidIP(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "ip", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "jwt") { + if (!isValidJWT(input.data, check.alg)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "jwt", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "cidr") { + if (!isValidCidr(input.data, check.version)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "cidr", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "base64") { + if (!base64Regex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "base64url") { + if (!base64urlRegex.test(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + validation: "base64url", + code: ZodIssueCode.invalid_string, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _regex(regex, validation, message) { + return this.refinement((data) => regex.test(data), { + validation, + code: ZodIssueCode.invalid_string, + ...errorUtil.errToObj(message) + }); + } + _addCheck(check) { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + email(message) { + return this._addCheck({ kind: "email", ...errorUtil.errToObj(message) }); + } + url(message) { + return this._addCheck({ kind: "url", ...errorUtil.errToObj(message) }); + } + emoji(message) { + return this._addCheck({ kind: "emoji", ...errorUtil.errToObj(message) }); + } + uuid(message) { + return this._addCheck({ kind: "uuid", ...errorUtil.errToObj(message) }); + } + nanoid(message) { + return this._addCheck({ kind: "nanoid", ...errorUtil.errToObj(message) }); + } + cuid(message) { + return this._addCheck({ kind: "cuid", ...errorUtil.errToObj(message) }); + } + cuid2(message) { + return this._addCheck({ kind: "cuid2", ...errorUtil.errToObj(message) }); + } + ulid(message) { + return this._addCheck({ kind: "ulid", ...errorUtil.errToObj(message) }); + } + base64(message) { + return this._addCheck({ kind: "base64", ...errorUtil.errToObj(message) }); + } + base64url(message) { + return this._addCheck({ + kind: "base64url", + ...errorUtil.errToObj(message) + }); + } + jwt(options) { + return this._addCheck({ kind: "jwt", ...errorUtil.errToObj(options) }); + } + ip(options) { + return this._addCheck({ kind: "ip", ...errorUtil.errToObj(options) }); + } + cidr(options) { + return this._addCheck({ kind: "cidr", ...errorUtil.errToObj(options) }); + } + datetime(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "datetime", + precision: null, + offset: false, + local: false, + message: options + }); + } + return this._addCheck({ + kind: "datetime", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + offset: options?.offset ?? false, + local: options?.local ?? false, + ...errorUtil.errToObj(options?.message) + }); + } + date(message) { + return this._addCheck({ kind: "date", message }); + } + time(options) { + if (typeof options === "string") { + return this._addCheck({ + kind: "time", + precision: null, + message: options + }); + } + return this._addCheck({ + kind: "time", + precision: typeof options?.precision === "undefined" ? null : options?.precision, + ...errorUtil.errToObj(options?.message) + }); + } + duration(message) { + return this._addCheck({ kind: "duration", ...errorUtil.errToObj(message) }); + } + regex(regex, message) { + return this._addCheck({ + kind: "regex", + regex, + ...errorUtil.errToObj(message) + }); + } + includes(value, options) { + return this._addCheck({ + kind: "includes", + value, + position: options?.position, + ...errorUtil.errToObj(options?.message) + }); + } + startsWith(value, message) { + return this._addCheck({ + kind: "startsWith", + value, + ...errorUtil.errToObj(message) + }); + } + endsWith(value, message) { + return this._addCheck({ + kind: "endsWith", + value, + ...errorUtil.errToObj(message) + }); + } + min(minLength, message) { + return this._addCheck({ + kind: "min", + value: minLength, + ...errorUtil.errToObj(message) + }); + } + max(maxLength, message) { + return this._addCheck({ + kind: "max", + value: maxLength, + ...errorUtil.errToObj(message) + }); + } + length(len, message) { + return this._addCheck({ + kind: "length", + value: len, + ...errorUtil.errToObj(message) + }); + } + /** + * Equivalent to `.min(1)` + */ + nonempty(message) { + return this.min(1, errorUtil.errToObj(message)); + } + trim() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }] + }); + } + toLowerCase() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }] + }); + } + toUpperCase() { + return new _ZodString({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }] + }); + } + get isDatetime() { + return !!this._def.checks.find((ch) => ch.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((ch) => ch.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((ch) => ch.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((ch) => ch.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((ch) => ch.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((ch) => ch.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((ch) => ch.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((ch) => ch.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((ch) => ch.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((ch) => ch.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((ch) => ch.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((ch) => ch.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((ch) => ch.kind === "ip"); + } + get isCIDR() { + return !!this._def.checks.find((ch) => ch.kind === "cidr"); + } + get isBase64() { + return !!this._def.checks.find((ch) => ch.kind === "base64"); + } + get isBase64url() { + return !!this._def.checks.find((ch) => ch.kind === "base64url"); + } + get minLength() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxLength() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +}; +ZodString.create = (params) => { + return new ZodString({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodString, + coerce: params?.coerce ?? false, + ...processCreateParams(params) + }); +}; +function floatSafeRemainder(val, step) { + const valDecCount = (val.toString().split(".")[1] || "").length; + const stepDecCount = (step.toString().split(".")[1] || "").length; + const decCount = valDecCount > stepDecCount ? valDecCount : stepDecCount; + const valInt = Number.parseInt(val.toFixed(decCount).replace(".", "")); + const stepInt = Number.parseInt(step.toFixed(decCount).replace(".", "")); + return valInt % stepInt / 10 ** decCount; +} +var ZodNumber = class _ZodNumber extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + this.step = this.multipleOf; + } + _parse(input) { + if (this._def.coerce) { + input.data = Number(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.number) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.number, + received: ctx2.parsedType + }); + return INVALID; + } + let ctx = void 0; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "int") { + if (!util2.isInteger(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: "integer", + received: "float", + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: check.value, + type: "number", + inclusive: check.inclusive, + exact: false, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (floatSafeRemainder(input.data, check.value) !== 0) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "finite") { + if (!Number.isFinite(input.data)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_finite, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new _ZodNumber({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message) + } + ] + }); + } + _addCheck(check) { + return new _ZodNumber({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + int(message) { + return this._addCheck({ + kind: "int", + message: errorUtil.toString(message) + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: false, + message: errorUtil.toString(message) + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: false, + message: errorUtil.toString(message) + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: true, + message: errorUtil.toString(message) + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: true, + message: errorUtil.toString(message) + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message) + }); + } + finite(message) { + return this._addCheck({ + kind: "finite", + message: errorUtil.toString(message) + }); + } + safe(message) { + return this._addCheck({ + kind: "min", + inclusive: true, + value: Number.MIN_SAFE_INTEGER, + message: errorUtil.toString(message) + })._addCheck({ + kind: "max", + inclusive: true, + value: Number.MAX_SAFE_INTEGER, + message: errorUtil.toString(message) + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } + get isInt() { + return !!this._def.checks.find((ch) => ch.kind === "int" || ch.kind === "multipleOf" && util2.isInteger(ch.value)); + } + get isFinite() { + let max = null; + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "finite" || ch.kind === "int" || ch.kind === "multipleOf") { + return true; + } else if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } else if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return Number.isFinite(min) && Number.isFinite(max); + } +}; +ZodNumber.create = (params) => { + return new ZodNumber({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodNumber, + coerce: params?.coerce || false, + ...processCreateParams(params) + }); +}; +var ZodBigInt = class _ZodBigInt extends ZodType { + constructor() { + super(...arguments); + this.min = this.gte; + this.max = this.lte; + } + _parse(input) { + if (this._def.coerce) { + try { + input.data = BigInt(input.data); + } catch { + return this._getInvalidInput(input); + } + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.bigint) { + return this._getInvalidInput(input); + } + let ctx = void 0; + const status = new ParseStatus(); + for (const check of this._def.checks) { + if (check.kind === "min") { + const tooSmall = check.inclusive ? input.data < check.value : input.data <= check.value; + if (tooSmall) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + type: "bigint", + minimum: check.value, + inclusive: check.inclusive, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "max") { + const tooBig = check.inclusive ? input.data > check.value : input.data >= check.value; + if (tooBig) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + type: "bigint", + maximum: check.value, + inclusive: check.inclusive, + message: check.message + }); + status.dirty(); + } + } else if (check.kind === "multipleOf") { + if (input.data % check.value !== BigInt(0)) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.not_multiple_of, + multipleOf: check.value, + message: check.message + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { status: status.value, value: input.data }; + } + _getInvalidInput(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.bigint, + received: ctx.parsedType + }); + return INVALID; + } + gte(value, message) { + return this.setLimit("min", value, true, errorUtil.toString(message)); + } + gt(value, message) { + return this.setLimit("min", value, false, errorUtil.toString(message)); + } + lte(value, message) { + return this.setLimit("max", value, true, errorUtil.toString(message)); + } + lt(value, message) { + return this.setLimit("max", value, false, errorUtil.toString(message)); + } + setLimit(kind, value, inclusive, message) { + return new _ZodBigInt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind, + value, + inclusive, + message: errorUtil.toString(message) + } + ] + }); + } + _addCheck(check) { + return new _ZodBigInt({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + positive(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message) + }); + } + negative(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: false, + message: errorUtil.toString(message) + }); + } + nonpositive(message) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message) + }); + } + nonnegative(message) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: true, + message: errorUtil.toString(message) + }); + } + multipleOf(value, message) { + return this._addCheck({ + kind: "multipleOf", + value, + message: errorUtil.toString(message) + }); + } + get minValue() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min; + } + get maxValue() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max; + } +}; +ZodBigInt.create = (params) => { + return new ZodBigInt({ + checks: [], + typeName: ZodFirstPartyTypeKind.ZodBigInt, + coerce: params?.coerce ?? false, + ...processCreateParams(params) + }); +}; +var ZodBoolean = class extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = Boolean(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.boolean) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.boolean, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodBoolean.create = (params) => { + return new ZodBoolean({ + typeName: ZodFirstPartyTypeKind.ZodBoolean, + coerce: params?.coerce || false, + ...processCreateParams(params) + }); +}; +var ZodDate = class _ZodDate extends ZodType { + _parse(input) { + if (this._def.coerce) { + input.data = new Date(input.data); + } + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.date) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.date, + received: ctx2.parsedType + }); + return INVALID; + } + if (Number.isNaN(input.data.getTime())) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_date + }); + return INVALID; + } + const status = new ParseStatus(); + let ctx = void 0; + for (const check of this._def.checks) { + if (check.kind === "min") { + if (input.data.getTime() < check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + message: check.message, + inclusive: true, + exact: false, + minimum: check.value, + type: "date" + }); + status.dirty(); + } + } else if (check.kind === "max") { + if (input.data.getTime() > check.value) { + ctx = this._getOrReturnCtx(input, ctx); + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + message: check.message, + inclusive: true, + exact: false, + maximum: check.value, + type: "date" + }); + status.dirty(); + } + } else { + util2.assertNever(check); + } + } + return { + status: status.value, + value: new Date(input.data.getTime()) + }; + } + _addCheck(check) { + return new _ZodDate({ + ...this._def, + checks: [...this._def.checks, check] + }); + } + min(minDate, message) { + return this._addCheck({ + kind: "min", + value: minDate.getTime(), + message: errorUtil.toString(message) + }); + } + max(maxDate, message) { + return this._addCheck({ + kind: "max", + value: maxDate.getTime(), + message: errorUtil.toString(message) + }); + } + get minDate() { + let min = null; + for (const ch of this._def.checks) { + if (ch.kind === "min") { + if (min === null || ch.value > min) + min = ch.value; + } + } + return min != null ? new Date(min) : null; + } + get maxDate() { + let max = null; + for (const ch of this._def.checks) { + if (ch.kind === "max") { + if (max === null || ch.value < max) + max = ch.value; + } + } + return max != null ? new Date(max) : null; + } +}; +ZodDate.create = (params) => { + return new ZodDate({ + checks: [], + coerce: params?.coerce || false, + typeName: ZodFirstPartyTypeKind.ZodDate, + ...processCreateParams(params) + }); +}; +var ZodSymbol = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.symbol) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.symbol, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodSymbol.create = (params) => { + return new ZodSymbol({ + typeName: ZodFirstPartyTypeKind.ZodSymbol, + ...processCreateParams(params) + }); +}; +var ZodUndefined = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.undefined, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodUndefined.create = (params) => { + return new ZodUndefined({ + typeName: ZodFirstPartyTypeKind.ZodUndefined, + ...processCreateParams(params) + }); +}; +var ZodNull = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.null) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.null, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodNull.create = (params) => { + return new ZodNull({ + typeName: ZodFirstPartyTypeKind.ZodNull, + ...processCreateParams(params) + }); +}; +var ZodAny = class extends ZodType { + constructor() { + super(...arguments); + this._any = true; + } + _parse(input) { + return OK(input.data); + } +}; +ZodAny.create = (params) => { + return new ZodAny({ + typeName: ZodFirstPartyTypeKind.ZodAny, + ...processCreateParams(params) + }); +}; +var ZodUnknown = class extends ZodType { + constructor() { + super(...arguments); + this._unknown = true; + } + _parse(input) { + return OK(input.data); + } +}; +ZodUnknown.create = (params) => { + return new ZodUnknown({ + typeName: ZodFirstPartyTypeKind.ZodUnknown, + ...processCreateParams(params) + }); +}; +var ZodNever = class extends ZodType { + _parse(input) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.never, + received: ctx.parsedType + }); + return INVALID; + } +}; +ZodNever.create = (params) => { + return new ZodNever({ + typeName: ZodFirstPartyTypeKind.ZodNever, + ...processCreateParams(params) + }); +}; +var ZodVoid = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.undefined) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.void, + received: ctx.parsedType + }); + return INVALID; + } + return OK(input.data); + } +}; +ZodVoid.create = (params) => { + return new ZodVoid({ + typeName: ZodFirstPartyTypeKind.ZodVoid, + ...processCreateParams(params) + }); +}; +var ZodArray = class _ZodArray extends ZodType { + _parse(input) { + const { ctx, status } = this._processInputParams(input); + const def = this._def; + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType + }); + return INVALID; + } + if (def.exactLength !== null) { + const tooBig = ctx.data.length > def.exactLength.value; + const tooSmall = ctx.data.length < def.exactLength.value; + if (tooBig || tooSmall) { + addIssueToContext(ctx, { + code: tooBig ? ZodIssueCode.too_big : ZodIssueCode.too_small, + minimum: tooSmall ? def.exactLength.value : void 0, + maximum: tooBig ? def.exactLength.value : void 0, + type: "array", + inclusive: true, + exact: true, + message: def.exactLength.message + }); + status.dirty(); + } + } + if (def.minLength !== null) { + if (ctx.data.length < def.minLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.minLength.message + }); + status.dirty(); + } + } + if (def.maxLength !== null) { + if (ctx.data.length > def.maxLength.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxLength.value, + type: "array", + inclusive: true, + exact: false, + message: def.maxLength.message + }); + status.dirty(); + } + } + if (ctx.common.async) { + return Promise.all([...ctx.data].map((item, i) => { + return def.type._parseAsync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + })).then((result2) => { + return ParseStatus.mergeArray(status, result2); + }); + } + const result = [...ctx.data].map((item, i) => { + return def.type._parseSync(new ParseInputLazyPath(ctx, item, ctx.path, i)); + }); + return ParseStatus.mergeArray(status, result); + } + get element() { + return this._def.type; + } + min(minLength, message) { + return new _ZodArray({ + ...this._def, + minLength: { value: minLength, message: errorUtil.toString(message) } + }); + } + max(maxLength, message) { + return new _ZodArray({ + ...this._def, + maxLength: { value: maxLength, message: errorUtil.toString(message) } + }); + } + length(len, message) { + return new _ZodArray({ + ...this._def, + exactLength: { value: len, message: errorUtil.toString(message) } + }); + } + nonempty(message) { + return this.min(1, message); + } +}; +ZodArray.create = (schema, params) => { + return new ZodArray({ + type: schema, + minLength: null, + maxLength: null, + exactLength: null, + typeName: ZodFirstPartyTypeKind.ZodArray, + ...processCreateParams(params) + }); +}; +function deepPartialify(schema) { + if (schema instanceof ZodObject) { + const newShape = {}; + for (const key in schema.shape) { + const fieldSchema = schema.shape[key]; + newShape[key] = ZodOptional.create(deepPartialify(fieldSchema)); + } + return new ZodObject({ + ...schema._def, + shape: () => newShape + }); + } else if (schema instanceof ZodArray) { + return new ZodArray({ + ...schema._def, + type: deepPartialify(schema.element) + }); + } else if (schema instanceof ZodOptional) { + return ZodOptional.create(deepPartialify(schema.unwrap())); + } else if (schema instanceof ZodNullable) { + return ZodNullable.create(deepPartialify(schema.unwrap())); + } else if (schema instanceof ZodTuple) { + return ZodTuple.create(schema.items.map((item) => deepPartialify(item))); + } else { + return schema; + } +} +var ZodObject = class _ZodObject extends ZodType { + constructor() { + super(...arguments); + this._cached = null; + this.nonstrict = this.passthrough; + this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const shape = this._def.shape(); + const keys = util2.objectKeys(shape); + this._cached = { shape, keys }; + return this._cached; + } + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.object) { + const ctx2 = this._getOrReturnCtx(input); + addIssueToContext(ctx2, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx2.parsedType + }); + return INVALID; + } + const { status, ctx } = this._processInputParams(input); + const { shape, keys: shapeKeys } = this._getCached(); + const extraKeys = []; + if (!(this._def.catchall instanceof ZodNever && this._def.unknownKeys === "strip")) { + for (const key in ctx.data) { + if (!shapeKeys.includes(key)) { + extraKeys.push(key); + } + } + } + const pairs = []; + for (const key of shapeKeys) { + const keyValidator = shape[key]; + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: keyValidator._parse(new ParseInputLazyPath(ctx, value, ctx.path, key)), + alwaysSet: key in ctx.data + }); + } + if (this._def.catchall instanceof ZodNever) { + const unknownKeys = this._def.unknownKeys; + if (unknownKeys === "passthrough") { + for (const key of extraKeys) { + pairs.push({ + key: { status: "valid", value: key }, + value: { status: "valid", value: ctx.data[key] } + }); + } + } else if (unknownKeys === "strict") { + if (extraKeys.length > 0) { + addIssueToContext(ctx, { + code: ZodIssueCode.unrecognized_keys, + keys: extraKeys + }); + status.dirty(); + } + } else if (unknownKeys === "strip") { + } else { + throw new Error(`Internal ZodObject error: invalid unknownKeys value.`); + } + } else { + const catchall = this._def.catchall; + for (const key of extraKeys) { + const value = ctx.data[key]; + pairs.push({ + key: { status: "valid", value: key }, + value: catchall._parse( + new ParseInputLazyPath(ctx, value, ctx.path, key) + //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: key in ctx.data + }); + } + } + if (ctx.common.async) { + return Promise.resolve().then(async () => { + const syncPairs = []; + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + syncPairs.push({ + key, + value, + alwaysSet: pair.alwaysSet + }); + } + return syncPairs; + }).then((syncPairs) => { + return ParseStatus.mergeObjectSync(status, syncPairs); + }); + } else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get shape() { + return this._def.shape(); + } + strict(message) { + errorUtil.errToObj; + return new _ZodObject({ + ...this._def, + unknownKeys: "strict", + ...message !== void 0 ? { + errorMap: (issue, ctx) => { + const defaultError = this._def.errorMap?.(issue, ctx).message ?? ctx.defaultError; + if (issue.code === "unrecognized_keys") + return { + message: errorUtil.errToObj(message).message ?? defaultError + }; + return { + message: defaultError + }; + } + } : {} + }); + } + strip() { + return new _ZodObject({ + ...this._def, + unknownKeys: "strip" + }); + } + passthrough() { + return new _ZodObject({ + ...this._def, + unknownKeys: "passthrough" + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(augmentation) { + return new _ZodObject({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...augmentation + }) + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(merging) { + const merged = new _ZodObject({ + unknownKeys: merging._def.unknownKeys, + catchall: merging._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...merging._def.shape() + }), + typeName: ZodFirstPartyTypeKind.ZodObject + }); + return merged; + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(key, schema) { + return this.augment({ [key]: schema }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(index) { + return new _ZodObject({ + ...this._def, + catchall: index + }); + } + pick(mask) { + const shape = {}; + for (const key of util2.objectKeys(mask)) { + if (mask[key] && this.shape[key]) { + shape[key] = this.shape[key]; + } + } + return new _ZodObject({ + ...this._def, + shape: () => shape + }); + } + omit(mask) { + const shape = {}; + for (const key of util2.objectKeys(this.shape)) { + if (!mask[key]) { + shape[key] = this.shape[key]; + } + } + return new _ZodObject({ + ...this._def, + shape: () => shape + }); + } + /** + * @deprecated + */ + deepPartial() { + return deepPartialify(this); + } + partial(mask) { + const newShape = {}; + for (const key of util2.objectKeys(this.shape)) { + const fieldSchema = this.shape[key]; + if (mask && !mask[key]) { + newShape[key] = fieldSchema; + } else { + newShape[key] = fieldSchema.optional(); + } + } + return new _ZodObject({ + ...this._def, + shape: () => newShape + }); + } + required(mask) { + const newShape = {}; + for (const key of util2.objectKeys(this.shape)) { + if (mask && !mask[key]) { + newShape[key] = this.shape[key]; + } else { + const fieldSchema = this.shape[key]; + let newField = fieldSchema; + while (newField instanceof ZodOptional) { + newField = newField._def.innerType; + } + newShape[key] = newField; + } + } + return new _ZodObject({ + ...this._def, + shape: () => newShape + }); + } + keyof() { + return createZodEnum(util2.objectKeys(this.shape)); + } +}; +ZodObject.create = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +ZodObject.strictCreate = (shape, params) => { + return new ZodObject({ + shape: () => shape, + unknownKeys: "strict", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +ZodObject.lazycreate = (shape, params) => { + return new ZodObject({ + shape, + unknownKeys: "strip", + catchall: ZodNever.create(), + typeName: ZodFirstPartyTypeKind.ZodObject, + ...processCreateParams(params) + }); +}; +var ZodUnion = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const options = this._def.options; + function handleResults(results) { + for (const result of results) { + if (result.result.status === "valid") { + return result.result; + } + } + for (const result of results) { + if (result.result.status === "dirty") { + ctx.common.issues.push(...result.ctx.common.issues); + return result.result; + } + } + const unionErrors = results.map((result) => new ZodError(result.ctx.common.issues)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors + }); + return INVALID; + } + if (ctx.common.async) { + return Promise.all(options.map(async (option) => { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + }, + parent: null + }; + return { + result: await option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: childCtx + }), + ctx: childCtx + }; + })).then(handleResults); + } else { + let dirty = void 0; + const issues = []; + for (const option of options) { + const childCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + }, + parent: null + }; + const result = option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: childCtx + }); + if (result.status === "valid") { + return result; + } else if (result.status === "dirty" && !dirty) { + dirty = { result, ctx: childCtx }; + } + if (childCtx.common.issues.length) { + issues.push(childCtx.common.issues); + } + } + if (dirty) { + ctx.common.issues.push(...dirty.ctx.common.issues); + return dirty.result; + } + const unionErrors = issues.map((issues2) => new ZodError(issues2)); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union, + unionErrors + }); + return INVALID; + } + } + get options() { + return this._def.options; + } +}; +ZodUnion.create = (types, params) => { + return new ZodUnion({ + options: types, + typeName: ZodFirstPartyTypeKind.ZodUnion, + ...processCreateParams(params) + }); +}; +var getDiscriminator = (type) => { + if (type instanceof ZodLazy) { + return getDiscriminator(type.schema); + } else if (type instanceof ZodEffects) { + return getDiscriminator(type.innerType()); + } else if (type instanceof ZodLiteral) { + return [type.value]; + } else if (type instanceof ZodEnum) { + return type.options; + } else if (type instanceof ZodNativeEnum) { + return util2.objectValues(type.enum); + } else if (type instanceof ZodDefault) { + return getDiscriminator(type._def.innerType); + } else if (type instanceof ZodUndefined) { + return [void 0]; + } else if (type instanceof ZodNull) { + return [null]; + } else if (type instanceof ZodOptional) { + return [void 0, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodNullable) { + return [null, ...getDiscriminator(type.unwrap())]; + } else if (type instanceof ZodBranded) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodReadonly) { + return getDiscriminator(type.unwrap()); + } else if (type instanceof ZodCatch) { + return getDiscriminator(type._def.innerType); + } else { + return []; + } +}; +var ZodDiscriminatedUnion = class _ZodDiscriminatedUnion extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType + }); + return INVALID; + } + const discriminator = this.discriminator; + const discriminatorValue = ctx.data[discriminator]; + const option = this.optionsMap.get(discriminatorValue); + if (!option) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [discriminator] + }); + return INVALID; + } + if (ctx.common.async) { + return option._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + } else { + return option._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + } + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(discriminator, options, params) { + const optionsMap = /* @__PURE__ */ new Map(); + for (const type of options) { + const discriminatorValues = getDiscriminator(type.shape[discriminator]); + if (!discriminatorValues.length) { + throw new Error(`A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`); + } + for (const value of discriminatorValues) { + if (optionsMap.has(value)) { + throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`); + } + optionsMap.set(value, type); + } + } + return new _ZodDiscriminatedUnion({ + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator, + options, + optionsMap, + ...processCreateParams(params) + }); + } +}; +function mergeValues(a, b) { + const aType = getParsedType(a); + const bType = getParsedType(b); + if (a === b) { + return { valid: true, data: a }; + } else if (aType === ZodParsedType.object && bType === ZodParsedType.object) { + const bKeys = util2.objectKeys(b); + const sharedKeys = util2.objectKeys(a).filter((key) => bKeys.indexOf(key) !== -1); + const newObj = { ...a, ...b }; + for (const key of sharedKeys) { + const sharedValue = mergeValues(a[key], b[key]); + if (!sharedValue.valid) { + return { valid: false }; + } + newObj[key] = sharedValue.data; + } + return { valid: true, data: newObj }; + } else if (aType === ZodParsedType.array && bType === ZodParsedType.array) { + if (a.length !== b.length) { + return { valid: false }; + } + const newArray = []; + for (let index = 0; index < a.length; index++) { + const itemA = a[index]; + const itemB = b[index]; + const sharedValue = mergeValues(itemA, itemB); + if (!sharedValue.valid) { + return { valid: false }; + } + newArray.push(sharedValue.data); + } + return { valid: true, data: newArray }; + } else if (aType === ZodParsedType.date && bType === ZodParsedType.date && +a === +b) { + return { valid: true, data: a }; + } else { + return { valid: false }; + } +} +var ZodIntersection = class extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const handleParsed = (parsedLeft, parsedRight) => { + if (isAborted(parsedLeft) || isAborted(parsedRight)) { + return INVALID; + } + const merged = mergeValues(parsedLeft.value, parsedRight.value); + if (!merged.valid) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_intersection_types + }); + return INVALID; + } + if (isDirty(parsedLeft) || isDirty(parsedRight)) { + status.dirty(); + } + return { status: status.value, value: merged.data }; + }; + if (ctx.common.async) { + return Promise.all([ + this._def.left._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }), + this._def.right._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }) + ]).then(([left, right]) => handleParsed(left, right)); + } else { + return handleParsed(this._def.left._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }), this._def.right._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + })); + } + } +}; +ZodIntersection.create = (left, right, params) => { + return new ZodIntersection({ + left, + right, + typeName: ZodFirstPartyTypeKind.ZodIntersection, + ...processCreateParams(params) + }); +}; +var ZodTuple = class _ZodTuple extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.array) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.array, + received: ctx.parsedType + }); + return INVALID; + } + if (ctx.data.length < this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: this._def.items.length, + inclusive: true, + exact: false, + type: "array" + }); + return INVALID; + } + const rest = this._def.rest; + if (!rest && ctx.data.length > this._def.items.length) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: this._def.items.length, + inclusive: true, + exact: false, + type: "array" + }); + status.dirty(); + } + const items = [...ctx.data].map((item, itemIndex) => { + const schema = this._def.items[itemIndex] || this._def.rest; + if (!schema) + return null; + return schema._parse(new ParseInputLazyPath(ctx, item, ctx.path, itemIndex)); + }).filter((x2) => !!x2); + if (ctx.common.async) { + return Promise.all(items).then((results) => { + return ParseStatus.mergeArray(status, results); + }); + } else { + return ParseStatus.mergeArray(status, items); + } + } + get items() { + return this._def.items; + } + rest(rest) { + return new _ZodTuple({ + ...this._def, + rest + }); + } +}; +ZodTuple.create = (schemas, params) => { + if (!Array.isArray(schemas)) { + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + } + return new ZodTuple({ + items: schemas, + typeName: ZodFirstPartyTypeKind.ZodTuple, + rest: null, + ...processCreateParams(params) + }); +}; +var ZodRecord = class _ZodRecord extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.object) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.object, + received: ctx.parsedType + }); + return INVALID; + } + const pairs = []; + const keyType = this._def.keyType; + const valueType = this._def.valueType; + for (const key in ctx.data) { + pairs.push({ + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, key)), + value: valueType._parse(new ParseInputLazyPath(ctx, ctx.data[key], ctx.path, key)), + alwaysSet: key in ctx.data + }); + } + if (ctx.common.async) { + return ParseStatus.mergeObjectAsync(status, pairs); + } else { + return ParseStatus.mergeObjectSync(status, pairs); + } + } + get element() { + return this._def.valueType; + } + static create(first, second, third) { + if (second instanceof ZodType) { + return new _ZodRecord({ + keyType: first, + valueType: second, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(third) + }); + } + return new _ZodRecord({ + keyType: ZodString.create(), + valueType: first, + typeName: ZodFirstPartyTypeKind.ZodRecord, + ...processCreateParams(second) + }); + } +}; +var ZodMap = class extends ZodType { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.map) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.map, + received: ctx.parsedType + }); + return INVALID; + } + const keyType = this._def.keyType; + const valueType = this._def.valueType; + const pairs = [...ctx.data.entries()].map(([key, value], index) => { + return { + key: keyType._parse(new ParseInputLazyPath(ctx, key, ctx.path, [index, "key"])), + value: valueType._parse(new ParseInputLazyPath(ctx, value, ctx.path, [index, "value"])) + }; + }); + if (ctx.common.async) { + const finalMap = /* @__PURE__ */ new Map(); + return Promise.resolve().then(async () => { + for (const pair of pairs) { + const key = await pair.key; + const value = await pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + }); + } else { + const finalMap = /* @__PURE__ */ new Map(); + for (const pair of pairs) { + const key = pair.key; + const value = pair.value; + if (key.status === "aborted" || value.status === "aborted") { + return INVALID; + } + if (key.status === "dirty" || value.status === "dirty") { + status.dirty(); + } + finalMap.set(key.value, value.value); + } + return { status: status.value, value: finalMap }; + } + } +}; +ZodMap.create = (keyType, valueType, params) => { + return new ZodMap({ + valueType, + keyType, + typeName: ZodFirstPartyTypeKind.ZodMap, + ...processCreateParams(params) + }); +}; +var ZodSet = class _ZodSet extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.set) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.set, + received: ctx.parsedType + }); + return INVALID; + } + const def = this._def; + if (def.minSize !== null) { + if (ctx.data.size < def.minSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_small, + minimum: def.minSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.minSize.message + }); + status.dirty(); + } + } + if (def.maxSize !== null) { + if (ctx.data.size > def.maxSize.value) { + addIssueToContext(ctx, { + code: ZodIssueCode.too_big, + maximum: def.maxSize.value, + type: "set", + inclusive: true, + exact: false, + message: def.maxSize.message + }); + status.dirty(); + } + } + const valueType = this._def.valueType; + function finalizeSet(elements2) { + const parsedSet = /* @__PURE__ */ new Set(); + for (const element of elements2) { + if (element.status === "aborted") + return INVALID; + if (element.status === "dirty") + status.dirty(); + parsedSet.add(element.value); + } + return { status: status.value, value: parsedSet }; + } + const elements = [...ctx.data.values()].map((item, i) => valueType._parse(new ParseInputLazyPath(ctx, item, ctx.path, i))); + if (ctx.common.async) { + return Promise.all(elements).then((elements2) => finalizeSet(elements2)); + } else { + return finalizeSet(elements); + } + } + min(minSize, message) { + return new _ZodSet({ + ...this._def, + minSize: { value: minSize, message: errorUtil.toString(message) } + }); + } + max(maxSize, message) { + return new _ZodSet({ + ...this._def, + maxSize: { value: maxSize, message: errorUtil.toString(message) } + }); + } + size(size, message) { + return this.min(size, message).max(size, message); + } + nonempty(message) { + return this.min(1, message); + } +}; +ZodSet.create = (valueType, params) => { + return new ZodSet({ + valueType, + minSize: null, + maxSize: null, + typeName: ZodFirstPartyTypeKind.ZodSet, + ...processCreateParams(params) + }); +}; +var ZodFunction = class _ZodFunction extends ZodType { + constructor() { + super(...arguments); + this.validate = this.implement; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.function) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.function, + received: ctx.parsedType + }); + return INVALID; + } + function makeArgsIssue(args, error) { + return makeIssue({ + data: args, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), + issueData: { + code: ZodIssueCode.invalid_arguments, + argumentsError: error + } + }); + } + function makeReturnsIssue(returns, error) { + return makeIssue({ + data: returns, + path: ctx.path, + errorMaps: [ctx.common.contextualErrorMap, ctx.schemaErrorMap, getErrorMap(), en_default].filter((x2) => !!x2), + issueData: { + code: ZodIssueCode.invalid_return_type, + returnTypeError: error + } + }); + } + const params = { errorMap: ctx.common.contextualErrorMap }; + const fn = ctx.data; + if (this._def.returns instanceof ZodPromise) { + const me2 = this; + return OK(async function(...args) { + const error = new ZodError([]); + const parsedArgs = await me2._def.args.parseAsync(args, params).catch((e) => { + error.addIssue(makeArgsIssue(args, e)); + throw error; + }); + const result = await Reflect.apply(fn, this, parsedArgs); + const parsedReturns = await me2._def.returns._def.type.parseAsync(result, params).catch((e) => { + error.addIssue(makeReturnsIssue(result, e)); + throw error; + }); + return parsedReturns; + }); + } else { + const me2 = this; + return OK(function(...args) { + const parsedArgs = me2._def.args.safeParse(args, params); + if (!parsedArgs.success) { + throw new ZodError([makeArgsIssue(args, parsedArgs.error)]); + } + const result = Reflect.apply(fn, this, parsedArgs.data); + const parsedReturns = me2._def.returns.safeParse(result, params); + if (!parsedReturns.success) { + throw new ZodError([makeReturnsIssue(result, parsedReturns.error)]); + } + return parsedReturns.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...items) { + return new _ZodFunction({ + ...this._def, + args: ZodTuple.create(items).rest(ZodUnknown.create()) + }); + } + returns(returnType) { + return new _ZodFunction({ + ...this._def, + returns: returnType + }); + } + implement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + strictImplement(func) { + const validatedFunc = this.parse(func); + return validatedFunc; + } + static create(args, returns, params) { + return new _ZodFunction({ + args: args ? args : ZodTuple.create([]).rest(ZodUnknown.create()), + returns: returns || ZodUnknown.create(), + typeName: ZodFirstPartyTypeKind.ZodFunction, + ...processCreateParams(params) + }); + } +}; +var ZodLazy = class extends ZodType { + get schema() { + return this._def.getter(); + } + _parse(input) { + const { ctx } = this._processInputParams(input); + const lazySchema = this._def.getter(); + return lazySchema._parse({ data: ctx.data, path: ctx.path, parent: ctx }); + } +}; +ZodLazy.create = (getter, params) => { + return new ZodLazy({ + getter, + typeName: ZodFirstPartyTypeKind.ZodLazy, + ...processCreateParams(params) + }); +}; +var ZodLiteral = class extends ZodType { + _parse(input) { + if (input.data !== this._def.value) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_literal, + expected: this._def.value + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } + get value() { + return this._def.value; + } +}; +ZodLiteral.create = (value, params) => { + return new ZodLiteral({ + value, + typeName: ZodFirstPartyTypeKind.ZodLiteral, + ...processCreateParams(params) + }); +}; +function createZodEnum(values, params) { + return new ZodEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodEnum, + ...processCreateParams(params) + }); +} +var ZodEnum = class _ZodEnum extends ZodType { + _parse(input) { + if (typeof input.data !== "string") { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + expected: util2.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(this._def.values); + } + if (!this._cache.has(input.data)) { + const ctx = this._getOrReturnCtx(input); + const expectedValues = this._def.values; + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues + }); + return INVALID; + } + return OK(input.data); + } + get options() { + return this._def.values; + } + get enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Values() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + get Enum() { + const enumValues = {}; + for (const val of this._def.values) { + enumValues[val] = val; + } + return enumValues; + } + extract(values, newDef = this._def) { + return _ZodEnum.create(values, { + ...this._def, + ...newDef + }); + } + exclude(values, newDef = this._def) { + return _ZodEnum.create(this.options.filter((opt) => !values.includes(opt)), { + ...this._def, + ...newDef + }); + } +}; +ZodEnum.create = createZodEnum; +var ZodNativeEnum = class extends ZodType { + _parse(input) { + const nativeEnumValues = util2.getValidEnumValues(this._def.values); + const ctx = this._getOrReturnCtx(input); + if (ctx.parsedType !== ZodParsedType.string && ctx.parsedType !== ZodParsedType.number) { + const expectedValues = util2.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + expected: util2.joinValues(expectedValues), + received: ctx.parsedType, + code: ZodIssueCode.invalid_type + }); + return INVALID; + } + if (!this._cache) { + this._cache = new Set(util2.getValidEnumValues(this._def.values)); + } + if (!this._cache.has(input.data)) { + const expectedValues = util2.objectValues(nativeEnumValues); + addIssueToContext(ctx, { + received: ctx.data, + code: ZodIssueCode.invalid_enum_value, + options: expectedValues + }); + return INVALID; + } + return OK(input.data); + } + get enum() { + return this._def.values; + } +}; +ZodNativeEnum.create = (values, params) => { + return new ZodNativeEnum({ + values, + typeName: ZodFirstPartyTypeKind.ZodNativeEnum, + ...processCreateParams(params) + }); +}; +var ZodPromise = class extends ZodType { + unwrap() { + return this._def.type; + } + _parse(input) { + const { ctx } = this._processInputParams(input); + if (ctx.parsedType !== ZodParsedType.promise && ctx.common.async === false) { + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.promise, + received: ctx.parsedType + }); + return INVALID; + } + const promisified = ctx.parsedType === ZodParsedType.promise ? ctx.data : Promise.resolve(ctx.data); + return OK(promisified.then((data) => { + return this._def.type.parseAsync(data, { + path: ctx.path, + errorMap: ctx.common.contextualErrorMap + }); + })); + } +}; +ZodPromise.create = (schema, params) => { + return new ZodPromise({ + type: schema, + typeName: ZodFirstPartyTypeKind.ZodPromise, + ...processCreateParams(params) + }); +}; +var ZodEffects = class extends ZodType { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === ZodFirstPartyTypeKind.ZodEffects ? this._def.schema.sourceType() : this._def.schema; + } + _parse(input) { + const { status, ctx } = this._processInputParams(input); + const effect = this._def.effect || null; + const checkCtx = { + addIssue: (arg) => { + addIssueToContext(ctx, arg); + if (arg.fatal) { + status.abort(); + } else { + status.dirty(); + } + }, + get path() { + return ctx.path; + } + }; + checkCtx.addIssue = checkCtx.addIssue.bind(checkCtx); + if (effect.type === "preprocess") { + const processed = effect.transform(ctx.data, checkCtx); + if (ctx.common.async) { + return Promise.resolve(processed).then(async (processed2) => { + if (status.value === "aborted") + return INVALID; + const result = await this._def.schema._parseAsync({ + data: processed2, + path: ctx.path, + parent: ctx + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + }); + } else { + if (status.value === "aborted") + return INVALID; + const result = this._def.schema._parseSync({ + data: processed, + path: ctx.path, + parent: ctx + }); + if (result.status === "aborted") + return INVALID; + if (result.status === "dirty") + return DIRTY(result.value); + if (status.value === "dirty") + return DIRTY(result.value); + return result; + } + } + if (effect.type === "refinement") { + const executeRefinement = (acc) => { + const result = effect.refinement(acc, checkCtx); + if (ctx.common.async) { + return Promise.resolve(result); + } + if (result instanceof Promise) { + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + } + return acc; + }; + if (ctx.common.async === false) { + const inner = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + executeRefinement(inner.value); + return { status: status.value, value: inner.value }; + } else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((inner) => { + if (inner.status === "aborted") + return INVALID; + if (inner.status === "dirty") + status.dirty(); + return executeRefinement(inner.value).then(() => { + return { status: status.value, value: inner.value }; + }); + }); + } + } + if (effect.type === "transform") { + if (ctx.common.async === false) { + const base = this._def.schema._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (!isValid(base)) + return INVALID; + const result = effect.transform(base.value, checkCtx); + if (result instanceof Promise) { + throw new Error(`Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead.`); + } + return { status: status.value, value: result }; + } else { + return this._def.schema._parseAsync({ data: ctx.data, path: ctx.path, parent: ctx }).then((base) => { + if (!isValid(base)) + return INVALID; + return Promise.resolve(effect.transform(base.value, checkCtx)).then((result) => ({ + status: status.value, + value: result + })); + }); + } + } + util2.assertNever(effect); + } +}; +ZodEffects.create = (schema, effect, params) => { + return new ZodEffects({ + schema, + typeName: ZodFirstPartyTypeKind.ZodEffects, + effect, + ...processCreateParams(params) + }); +}; +ZodEffects.createWithPreprocess = (preprocess, schema, params) => { + return new ZodEffects({ + schema, + effect: { type: "preprocess", transform: preprocess }, + typeName: ZodFirstPartyTypeKind.ZodEffects, + ...processCreateParams(params) + }); +}; +var ZodOptional = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.undefined) { + return OK(void 0); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +}; +ZodOptional.create = (type, params) => { + return new ZodOptional({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodOptional, + ...processCreateParams(params) + }); +}; +var ZodNullable = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType === ZodParsedType.null) { + return OK(null); + } + return this._def.innerType._parse(input); + } + unwrap() { + return this._def.innerType; + } +}; +ZodNullable.create = (type, params) => { + return new ZodNullable({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodNullable, + ...processCreateParams(params) + }); +}; +var ZodDefault = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + let data = ctx.data; + if (ctx.parsedType === ZodParsedType.undefined) { + data = this._def.defaultValue(); + } + return this._def.innerType._parse({ + data, + path: ctx.path, + parent: ctx + }); + } + removeDefault() { + return this._def.innerType; + } +}; +ZodDefault.create = (type, params) => { + return new ZodDefault({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodDefault, + defaultValue: typeof params.default === "function" ? params.default : () => params.default, + ...processCreateParams(params) + }); +}; +var ZodCatch = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const newCtx = { + ...ctx, + common: { + ...ctx.common, + issues: [] + } + }; + const result = this._def.innerType._parse({ + data: newCtx.data, + path: newCtx.path, + parent: { + ...newCtx + } + }); + if (isAsync(result)) { + return result.then((result2) => { + return { + status: "valid", + value: result2.status === "valid" ? result2.value : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data + }) + }; + }); + } else { + return { + status: "valid", + value: result.status === "valid" ? result.value : this._def.catchValue({ + get error() { + return new ZodError(newCtx.common.issues); + }, + input: newCtx.data + }) + }; + } + } + removeCatch() { + return this._def.innerType; + } +}; +ZodCatch.create = (type, params) => { + return new ZodCatch({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodCatch, + catchValue: typeof params.catch === "function" ? params.catch : () => params.catch, + ...processCreateParams(params) + }); +}; +var ZodNaN = class extends ZodType { + _parse(input) { + const parsedType = this._getType(input); + if (parsedType !== ZodParsedType.nan) { + const ctx = this._getOrReturnCtx(input); + addIssueToContext(ctx, { + code: ZodIssueCode.invalid_type, + expected: ZodParsedType.nan, + received: ctx.parsedType + }); + return INVALID; + } + return { status: "valid", value: input.data }; + } +}; +ZodNaN.create = (params) => { + return new ZodNaN({ + typeName: ZodFirstPartyTypeKind.ZodNaN, + ...processCreateParams(params) + }); +}; +var BRAND = /* @__PURE__ */ Symbol("zod_brand"); +var ZodBranded = class extends ZodType { + _parse(input) { + const { ctx } = this._processInputParams(input); + const data = ctx.data; + return this._def.type._parse({ + data, + path: ctx.path, + parent: ctx + }); + } + unwrap() { + return this._def.type; + } +}; +var ZodPipeline = class _ZodPipeline extends ZodType { + _parse(input) { + const { status, ctx } = this._processInputParams(input); + if (ctx.common.async) { + const handleAsync = async () => { + const inResult = await this._def.in._parseAsync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return DIRTY(inResult.value); + } else { + return this._def.out._parseAsync({ + data: inResult.value, + path: ctx.path, + parent: ctx + }); + } + }; + return handleAsync(); + } else { + const inResult = this._def.in._parseSync({ + data: ctx.data, + path: ctx.path, + parent: ctx + }); + if (inResult.status === "aborted") + return INVALID; + if (inResult.status === "dirty") { + status.dirty(); + return { + status: "dirty", + value: inResult.value + }; + } else { + return this._def.out._parseSync({ + data: inResult.value, + path: ctx.path, + parent: ctx + }); + } + } + } + static create(a, b) { + return new _ZodPipeline({ + in: a, + out: b, + typeName: ZodFirstPartyTypeKind.ZodPipeline + }); + } +}; +var ZodReadonly = class extends ZodType { + _parse(input) { + const result = this._def.innerType._parse(input); + const freeze = (data) => { + if (isValid(data)) { + data.value = Object.freeze(data.value); + } + return data; + }; + return isAsync(result) ? result.then((data) => freeze(data)) : freeze(result); + } + unwrap() { + return this._def.innerType; + } +}; +ZodReadonly.create = (type, params) => { + return new ZodReadonly({ + innerType: type, + typeName: ZodFirstPartyTypeKind.ZodReadonly, + ...processCreateParams(params) + }); +}; +function cleanParams(params, data) { + const p = typeof params === "function" ? params(data) : typeof params === "string" ? { message: params } : params; + const p2 = typeof p === "string" ? { message: p } : p; + return p2; +} +function custom(check, _params = {}, fatal) { + if (check) + return ZodAny.create().superRefine((data, ctx) => { + const r = check(data); + if (r instanceof Promise) { + return r.then((r2) => { + if (!r2) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + }); + } + if (!r) { + const params = cleanParams(_params, data); + const _fatal = params.fatal ?? fatal ?? true; + ctx.addIssue({ code: "custom", ...params, fatal: _fatal }); + } + return; + }); + return ZodAny.create(); +} +var late = { + object: ZodObject.lazycreate +}; +var ZodFirstPartyTypeKind; +(function(ZodFirstPartyTypeKind2) { + ZodFirstPartyTypeKind2["ZodString"] = "ZodString"; + ZodFirstPartyTypeKind2["ZodNumber"] = "ZodNumber"; + ZodFirstPartyTypeKind2["ZodNaN"] = "ZodNaN"; + ZodFirstPartyTypeKind2["ZodBigInt"] = "ZodBigInt"; + ZodFirstPartyTypeKind2["ZodBoolean"] = "ZodBoolean"; + ZodFirstPartyTypeKind2["ZodDate"] = "ZodDate"; + ZodFirstPartyTypeKind2["ZodSymbol"] = "ZodSymbol"; + ZodFirstPartyTypeKind2["ZodUndefined"] = "ZodUndefined"; + ZodFirstPartyTypeKind2["ZodNull"] = "ZodNull"; + ZodFirstPartyTypeKind2["ZodAny"] = "ZodAny"; + ZodFirstPartyTypeKind2["ZodUnknown"] = "ZodUnknown"; + ZodFirstPartyTypeKind2["ZodNever"] = "ZodNever"; + ZodFirstPartyTypeKind2["ZodVoid"] = "ZodVoid"; + ZodFirstPartyTypeKind2["ZodArray"] = "ZodArray"; + ZodFirstPartyTypeKind2["ZodObject"] = "ZodObject"; + ZodFirstPartyTypeKind2["ZodUnion"] = "ZodUnion"; + ZodFirstPartyTypeKind2["ZodDiscriminatedUnion"] = "ZodDiscriminatedUnion"; + ZodFirstPartyTypeKind2["ZodIntersection"] = "ZodIntersection"; + ZodFirstPartyTypeKind2["ZodTuple"] = "ZodTuple"; + ZodFirstPartyTypeKind2["ZodRecord"] = "ZodRecord"; + ZodFirstPartyTypeKind2["ZodMap"] = "ZodMap"; + ZodFirstPartyTypeKind2["ZodSet"] = "ZodSet"; + ZodFirstPartyTypeKind2["ZodFunction"] = "ZodFunction"; + ZodFirstPartyTypeKind2["ZodLazy"] = "ZodLazy"; + ZodFirstPartyTypeKind2["ZodLiteral"] = "ZodLiteral"; + ZodFirstPartyTypeKind2["ZodEnum"] = "ZodEnum"; + ZodFirstPartyTypeKind2["ZodEffects"] = "ZodEffects"; + ZodFirstPartyTypeKind2["ZodNativeEnum"] = "ZodNativeEnum"; + ZodFirstPartyTypeKind2["ZodOptional"] = "ZodOptional"; + ZodFirstPartyTypeKind2["ZodNullable"] = "ZodNullable"; + ZodFirstPartyTypeKind2["ZodDefault"] = "ZodDefault"; + ZodFirstPartyTypeKind2["ZodCatch"] = "ZodCatch"; + ZodFirstPartyTypeKind2["ZodPromise"] = "ZodPromise"; + ZodFirstPartyTypeKind2["ZodBranded"] = "ZodBranded"; + ZodFirstPartyTypeKind2["ZodPipeline"] = "ZodPipeline"; + ZodFirstPartyTypeKind2["ZodReadonly"] = "ZodReadonly"; +})(ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {})); +var instanceOfType = (cls, params = { + message: `Input not instance of ${cls.name}` +}) => custom((data) => data instanceof cls, params); +var stringType = ZodString.create; +var numberType = ZodNumber.create; +var nanType = ZodNaN.create; +var bigIntType = ZodBigInt.create; +var booleanType = ZodBoolean.create; +var dateType = ZodDate.create; +var symbolType = ZodSymbol.create; +var undefinedType = ZodUndefined.create; +var nullType = ZodNull.create; +var anyType = ZodAny.create; +var unknownType = ZodUnknown.create; +var neverType = ZodNever.create; +var voidType = ZodVoid.create; +var arrayType = ZodArray.create; +var objectType = ZodObject.create; +var strictObjectType = ZodObject.strictCreate; +var unionType = ZodUnion.create; +var discriminatedUnionType = ZodDiscriminatedUnion.create; +var intersectionType = ZodIntersection.create; +var tupleType = ZodTuple.create; +var recordType = ZodRecord.create; +var mapType = ZodMap.create; +var setType = ZodSet.create; +var functionType = ZodFunction.create; +var lazyType = ZodLazy.create; +var literalType = ZodLiteral.create; +var enumType = ZodEnum.create; +var nativeEnumType = ZodNativeEnum.create; +var promiseType = ZodPromise.create; +var effectsType = ZodEffects.create; +var optionalType = ZodOptional.create; +var nullableType = ZodNullable.create; +var preprocessType = ZodEffects.createWithPreprocess; +var pipelineType = ZodPipeline.create; +var ostring = () => stringType().optional(); +var onumber = () => numberType().optional(); +var oboolean = () => booleanType().optional(); +var coerce = { + string: ((arg) => ZodString.create({ ...arg, coerce: true })), + number: ((arg) => ZodNumber.create({ ...arg, coerce: true })), + boolean: ((arg) => ZodBoolean.create({ + ...arg, + coerce: true + })), + bigint: ((arg) => ZodBigInt.create({ ...arg, coerce: true })), + date: ((arg) => ZodDate.create({ ...arg, coerce: true })) +}; +var NEVER = INVALID; + +// ../plugins/dist/utils.js +var isHookTree = (value) => { + if (value == null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + for (const child of Object.values(value)) { + if (child === void 0) { + continue; + } + if (typeof child === "function") { + continue; + } + if (child == null || typeof child !== "object" || Array.isArray(child) || !isHookTree(child)) { + return false; + } + } + return true; +}; + +// ../plugins/dist/plugin.js +var isHarnessPlugin = (value) => { + if (value == null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const candidate = value; + if (typeof candidate.name !== "string" || candidate.name.length === 0) { + return false; + } + if (candidate.createState != null && typeof candidate.createState !== "function") { + return false; + } + if (candidate.hooks != null && !isHookTree(candidate.hooks)) { + return false; + } + return true; +}; + +// ../plugins/dist/manager.js +var pluginsLogger = logger.child("plugins"); + +// ../config/dist/types.js +var DEFAULT_METRO_PORT = 8081; +var configLogger = logger.child("config"); +var RunnerSchema = external_exports.object({ + name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"), + config: external_exports.record(external_exports.any()), + runner: external_exports.string(), + cli: external_exports.string().optional(), + platformId: external_exports.string() +}); +var PluginSchema = external_exports.custom((value) => isHarnessPlugin(value), "Invalid Harness plugin"); +var ConfigSchema = external_exports.object({ + entryPoint: external_exports.string().min(1, "Entry point is required"), + appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"), + runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"), + plugins: external_exports.array(PluginSchema).optional().default([]), + defaultRunner: external_exports.string().optional(), + host: external_exports.string().min(1, "Host is required").optional(), + metroPort: external_exports.number().int("Metro port must be an integer").min(1, "Metro port must be at least 1").max(65535, "Metro port must be at most 65535").optional().default(DEFAULT_METRO_PORT), + webSocketPort: external_exports.number().optional().describe("Deprecated. Bridge traffic now uses metroPort and this value is ignored."), + bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4), + testTimeout: external_exports.number().min(1e3, "Test timeout must be at least 1 second").default(5e3), + platformReadyTimeout: external_exports.number().min(1e3, "Platform ready timeout must be at least 1 second").default(3e5), + bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(6e4), + maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2), + eagerPrewarm: external_exports.boolean().optional().default(true).describe("Start building the Metro bundle while the platform (emulator, simulator, or browser) is still booting, so the first bundle is ready sooner. Disable to defer the first bundle build until app startup."), + resetEnvironmentBetweenTestFiles: external_exports.union([external_exports.boolean(), external_exports.enum(["process", "runtime"])]).optional().default(true).describe("Controls how the environment is reset between test files. `true` (default) and `'process'` kill and cold-restart the app process. `'runtime'` reloads the JS runtime in place (DevSettings.reload() / window.location.reload()), which is cheaper but escalates to a process restart if the reload fails or the app does not reconnect in time. `false` disables resetting the environment between test files entirely."), + skipAlreadyIncludedModules: external_exports.boolean().optional().describe('Skip re-sending modules already served in the main app bundle when Metro serves a per-test-file bundle. Defaults to true; set to false as an escape hatch if it causes issues. Left undefined (rather than defaulted here) so `resolveSkipAlreadyIncludedModules` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__skipAlreadyIncludedModules` alias.'), + // Deprecated alias for `skipAlreadyIncludedModules` -- see + // `resolveSkipAlreadyIncludedModules`. Left without a `.default()` for the + // same reason: a default value would be indistinguishable from the user + // never having set it, which would break alias-vs-explicit-flag precedence. + unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().describe("Deprecated. Use `skipAlreadyIncludedModules` instead."), + cache: external_exports.object({ + metro: external_exports.boolean().optional().describe('Enable persistent Metro transform and file-map caching under the .harness cache directory. Defaults to true; set to false to always start with a cold Metro cache. Left undefined (rather than defaulted here) so `resolveMetroCacheEnabled` can tell "not set" apart from "explicitly set" when reconciling with the deprecated `unstable__enableMetroCache` alias.'), + version: external_exports.string().optional().describe("User-controlled salt folded into the Metro cacheVersion. Bump it to force-invalidate previously cached transforms.") + }).optional(), + // Deprecated alias for `cache.metro` -- see `resolveMetroCacheEnabled`. + // Left without a `.default()` for the same reason as + // `unstable__skipAlreadyIncludedModules`: a default value would be + // indistinguishable from the user never having set it, which would break + // alias-vs-explicit-flag precedence. + unstable__enableMetroCache: external_exports.boolean().optional().describe("Deprecated. Use `cache.metro` instead."), + permissions: external_exports.boolean().optional().default(false).describe("Enable platform-specific permission prompt automation. When false, Harness does not start permission-handling helpers such as the iOS XCTest agent."), + detectNativeCrashes: external_exports.boolean().optional().default(true), + crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500), + disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."), + coverage: external_exports.object({ + root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`), + native: external_exports.object({ + ios: external_exports.object({ + pods: external_exports.array(external_exports.string()).min(1, "At least one pod name is required").describe("Pod names to instrument for native code coverage. Coverage flags are injected at pod install time via a CocoaPods hook. After tests, profraw data is collected and converted to lcov format.") + }).optional() + }).optional().describe("Native code coverage configuration.") + }).optional(), + forwardClientLogs: external_exports.boolean().optional().default(false).describe("Enable forwarding of console.log, console.warn, console.error, and other console method calls from the React Native app during the active test run. When enabled, app console output is attached to the active test result's console output."), + diagnostics: external_exports.boolean().optional().default(false).describe("Enable diagnostics tracing for the harness session. Records spans for session setup, Metro bundling, bridge/client events, and per-file test runs, then writes a Chrome Trace Event JSON file and prints a summary after each run. Can also be enabled via the RN_HARNESS_DIAGNOSTICS environment variable."), + // Deprecated property - used for migration detection + include: external_exports.array(external_exports.string()).optional() +}).refine((config) => { + if (config.defaultRunner) { + return config.runners.some((runner) => runner.name === config.defaultRunner); + } + return true; +}, { + message: "Default runner must match one of the configured runner names", + path: ["defaultRunner"] +}); + +// ../config/dist/errors.js +var ConfigValidationError = class extends HarnessError { + filePath; + validationErrors; + constructor(filePath, validationErrors) { + const lines = [ + `Configuration validation failed in ${filePath}`, + "", + "The following issues were found:", + "" + ]; + validationErrors.forEach((error, index) => { + lines.push(` ${index + 1}. ${error}`); + }); + lines.push("", "Please fix these issues and try again.", "For more information, visit: https://react-native-harness.dev/docs/configuration"); + super(lines.join("\n")); + this.filePath = filePath; + this.validationErrors = validationErrors; + this.name = "ConfigValidationError"; + } +}; +var ConfigNotFoundError = class extends HarnessError { + searchPath; + constructor(searchPath) { + const lines = [ + "Configuration file not found", + "", + `Searched for configuration files in: ${searchPath}`, + "and all parent directories.", + "", + "React Native Harness looks for one of these files:", + " \u2022 rn-harness.config.js", + " \u2022 rn-harness.config.mjs", + " \u2022 rn-harness.config.cjs", + " \u2022 rn-harness.config.json", + "", + "For more information, visit: https://www.react-native-harness.dev/docs/getting-started/configuration" + ]; + super(lines.join("\n")); + this.searchPath = searchPath; + this.name = "ConfigNotFoundError"; + } +}; +var ConfigLoadError = class extends HarnessError { + filePath; + cause; + constructor(filePath, cause) { + const lines = [ + "Failed to load configuration file", + "", + `File: ${filePath}`, + "" + ]; + if (cause) { + lines.push("Error details:"); + lines.push(` ${cause.message}`); + lines.push(""); + } + lines.push("This could be due to:", " \u2022 Syntax errors in your configuration file", " \u2022 Missing dependencies or modules", " \u2022 Invalid file format or encoding", " \u2022 File permissions issues", "", "Troubleshooting steps:", " 1. Check the file syntax and format", " 2. Ensure all required dependencies are installed", " 3. Verify file permissions", " 4. Try creating a new configuration file", "", "For more help, visit: https://www.react-native-harness.dev/docs/getting-started/configuration"); + super(lines.join("\n")); + this.filePath = filePath; + this.name = "ConfigLoadError"; + this.cause = cause; + } +}; + +// ../config/dist/reader.js +var import_node_path10 = __toESM(require("path"), 1); +var import_node_fs10 = __toESM(require("fs"), 1); +var import_node_module2 = require("module"); +var import_meta = {}; +var extensions = [".js", ".mjs", ".cjs", ".json"]; +var importUp = async (dir, name) => { + const filePath = import_node_path10.default.join(dir, name); + for (const ext of extensions) { + const filePathWithExt = `${filePath}${ext}`; + if (import_node_fs10.default.existsSync(filePathWithExt)) { + let rawConfig; + try { + if (ext === ".mjs") { + rawConfig = await import(filePathWithExt).then((module2) => module2.default); + } else { + const require2 = (0, import_node_module2.createRequire)(import_meta.url); + rawConfig = require2(filePathWithExt); + } + } catch (error) { + throw new ConfigLoadError(filePathWithExt, error instanceof Error ? error : void 0); + } + try { + const config = ConfigSchema.parse(rawConfig); + return { config, filePathWithExt, configDir: dir }; + } catch (error) { + if (error instanceof ZodError) { + const validationErrors = error.errors.map((err) => { + const path11 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : ""; + return `${err.message}${path11}`; + }); + throw new ConfigValidationError(filePathWithExt, validationErrors); + } + throw error; + } + } + } + const parentDir = import_node_path10.default.dirname(dir); + if (parentDir === dir) { + throw new ConfigNotFoundError(dir); + } + return importUp(parentDir, name); +}; +var getConfig = async (dir) => { + const { config, configDir } = await importUp(dir, "rn-harness.config"); + return { + config, + projectRoot: configDir + }; +}; + +// src/shared/snapshot-metro.ts +var run = async () => { + try { + const projectRootInput = process.env.INPUT_PROJECTROOT; + const projectRoot = projectRootInput ? import_node_path11.default.resolve(projectRootInput) : process.cwd(); + console.info(`Snapshotting Metro cache for: ${projectRoot}`); + const { projectRoot: resolvedProjectRoot } = await getConfig(projectRoot); + const githubOutput = process.env.GITHUB_OUTPUT; + if (!githubOutput) { + throw new Error("GITHUB_OUTPUT environment variable is not set"); + } + const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); + const snapshotAfterRestore = cache.snapshot(); + const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)} +`; + import_node_fs11.default.appendFileSync(githubOutput, output); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } else { + console.error("Failed to snapshot the Metro cache"); + } + process.exit(1); + } +}; +run(); diff --git a/packages/cache/src/__tests__/domains/metro.test.ts b/packages/cache/src/__tests__/domains/metro.test.ts new file mode 100644 index 00000000..58f3aad8 --- /dev/null +++ b/packages/cache/src/__tests__/domains/metro.test.ts @@ -0,0 +1,156 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { computeMetroStaticInputs } from '../../domains/metro.js'; + +describe('computeMetroStaticInputs', () => { + let repoRoot: string; + + beforeEach(() => { + repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-metro-')); + }); + + afterEach(() => { + fs.rmSync(repoRoot, { recursive: true, force: true }); + }); + + it('only hashes recognized key filenames, ignoring unrelated files', () => { + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + fs.mkdirSync(path.join(repoRoot, 'nested'), { recursive: true }); + fs.writeFileSync( + path.join(repoRoot, 'nested', 'metro.config.js'), + 'config-a' + ); + fs.writeFileSync(path.join(repoRoot, 'README.md'), 'irrelevant'); + + const withoutReadme = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(repoRoot, 'README.md'), 'changed irrelevant'); + + const withChangedReadme = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(withoutReadme.lockfileHash).toBe(withChangedReadme.lockfileHash); + }); + + it('excludes matched files vendored under node_modules', () => { + // This is the correctness fix over today's + // `hashFiles('**/pnpm-lock.yaml', ...)`, which globs inside + // node_modules and would pick up a dependency's own lockfile. + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + const vendoredDir = path.join(repoRoot, 'node_modules', 'some-pkg'); + fs.mkdirSync(vendoredDir, { recursive: true }); + fs.writeFileSync( + path.join(vendoredDir, 'package-lock.json'), + 'vendored-lockfile' + ); + + const withVendored = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.rmSync(path.join(repoRoot, 'node_modules'), { + recursive: true, + force: true, + }); + + const withoutVendored = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(withVendored.lockfileHash).toBe(withoutVendored.lockfileHash); + }); + + it('is deterministic across independent calls given the same fixture tree', () => { + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + fs.writeFileSync(path.join(repoRoot, 'metro.config.js'), 'config-a'); + + const first = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + const second = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(first.lockfileHash).toBe(second.lockfileHash); + }); + + it('changes the hash when a matched file changes content', () => { + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + + const before = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-b'); + + const after = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(before.lockfileHash).not.toBe(after.lockfileHash); + }); + + it('does not change the hash when an unrelated file changes content', () => { + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + fs.writeFileSync(path.join(repoRoot, 'notes.txt'), 'v1'); + + const before = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(repoRoot, 'notes.txt'), 'v2'); + + const after = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(before.lockfileHash).toBe(after.lockfileHash); + }); + + it('passes bundlerMetroVersion and salt through into the returned object', () => { + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + + const withoutSalt = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '2.3.4', + }); + expect(withoutSalt.bundlerMetroVersion).toBe('2.3.4'); + expect(withoutSalt.salt).toBeUndefined(); + + const withSalt = computeMetroStaticInputs({ + repoRoot, + bundlerMetroVersion: '2.3.4', + salt: 'v2', + }); + expect(withSalt.salt).toBe('v2'); + }); + + it('returns a stable fallback instead of throwing when repoRoot does not exist', () => { + const missingRoot = path.join(repoRoot, 'does-not-exist'); + + const result = computeMetroStaticInputs({ + repoRoot: missingRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(result).toEqual({ + lockfileHash: 'unavailable', + bundlerMetroVersion: '1.0.0', + }); + }); +}); diff --git a/packages/cache/src/__tests__/plan.test.ts b/packages/cache/src/__tests__/plan.test.ts new file mode 100644 index 00000000..ea9bbd88 --- /dev/null +++ b/packages/cache/src/__tests__/plan.test.ts @@ -0,0 +1,330 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createHarnessCachePaths } from '../paths.js'; +import { + planRestore, + planSave, + snapshot, + writeKeysFile, + type CacheKeyInputs, + type CacheSnapshot, +} from '../plan.js'; + +describe('planRestore', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + const inputs = ( + staticInputs: Record + ): Record<'metro', CacheKeyInputs> => ({ + metro: { os: 'linux', staticInputs }, + }); + + it('produces the same restoreKey across independent calls given the same staticInputs', () => { + const paths = createHarnessCachePaths(projectRoot); + + const first = planRestore(paths, inputs({ a: '1', b: '2' })); + const second = planRestore(paths, inputs({ a: '1', b: '2' })); + + expect(first.domains.metro?.restoreKey).toBe( + second.domains.metro?.restoreKey + ); + }); + + it('produces the same restoreKey regardless of staticInputs key order', () => { + const paths = createHarnessCachePaths(projectRoot); + + const first = planRestore(paths, inputs({ a: '1', b: '2' })); + const second = planRestore(paths, inputs({ b: '2', a: '1' })); + + expect(first.domains.metro?.restoreKey).toBe( + second.domains.metro?.restoreKey + ); + }); + + it('produces a different restoreKey for different staticInputs', () => { + const paths = createHarnessCachePaths(projectRoot); + + const first = planRestore(paths, inputs({ a: '1' })); + const second = planRestore(paths, inputs({ a: '2' })); + + expect(first.domains.metro?.restoreKey).not.toBe( + second.domains.metro?.restoreKey + ); + }); + + it('shapes restorePrefixes for the accretive metro domain as a single trailing-dash prefix', () => { + const paths = createHarnessCachePaths(projectRoot); + const plan = planRestore(paths, inputs({ a: '1' })); + const domain = plan.domains.metro; + + expect(domain?.accretive).toBe(true); + expect(domain?.restorePrefixes).toEqual([`${domain?.restoreKey}-`]); + }); + + it('only includes domains present in the inputs', () => { + const paths = createHarnessCachePaths(projectRoot); + const plan = planRestore(paths, {}); + + expect(plan.domains).toEqual({}); + }); + + it('reports the domain cache directories as paths', () => { + const paths = createHarnessCachePaths(projectRoot); + const plan = planRestore(paths, inputs({ a: '1' })); + + expect(plan.domains.metro?.paths).toEqual([ + paths.metroTransform, + paths.metroFileMap, + ]); + }); +}); + +describe('snapshot', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + it('is empty for a domain whose directories do not exist', () => { + const paths = createHarnessCachePaths(projectRoot); + + expect(() => snapshot(paths)).not.toThrow(); + expect(snapshot(paths).metro).toEqual([]); + }); + + it('collects entries for files under the domain directories', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(path.join(paths.metroTransform, 'nested'), { + recursive: true, + }); + fs.writeFileSync(path.join(paths.metroTransform, 'a.txt'), 'aaa'); + fs.writeFileSync( + path.join(paths.metroTransform, 'nested', 'b.txt'), + 'bb' + ); + + const result = snapshot(paths); + const entries = result.metro ?? []; + + expect(entries).toHaveLength(2); + const aEntry = entries.find((entry) => + entry.path.endsWith(path.join('metro', 'a.txt')) + ); + const bEntry = entries.find((entry) => + entry.path.endsWith(path.join('metro', 'nested', 'b.txt')) + ); + expect(aEntry?.sizeBytes).toBe(3); + expect(bEntry?.sizeBytes).toBe(2); + expect(typeof aEntry?.mtimeMs).toBe('number'); + }); +}); + +describe('planSave', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + const inputs = ( + staticInputs: Record = { a: '1' } + ): Record<'metro', CacheKeyInputs> => ({ + metro: { os: 'linux', staticInputs }, + }); + + it('does not save when there is no change, regardless of policy', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + fs.writeFileSync(path.join(paths.metroTransform, 'a.txt'), 'aaa'); + + const before = snapshot(paths); + const plan = planSave( + paths, + before, + { mode: 'always', isDefaultBranch: true }, + inputs() + ); + + expect(plan.domains.metro?.shouldSave).toBe(false); + expect(plan.domains.metro?.delta).toEqual({ + addedEntries: 0, + removedEntries: 0, + addedBytes: 0, + }); + }); + + it('saves an added file only when policy allows it', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + const before = snapshot(paths); + + fs.writeFileSync(path.join(paths.metroTransform, 'a.txt'), 'aaa'); + + const disallowed = planSave( + paths, + before, + { mode: 'default-branch', isDefaultBranch: false }, + inputs() + ); + expect(disallowed.domains.metro?.shouldSave).toBe(false); + expect(disallowed.domains.metro?.delta.addedEntries).toBe(1); + expect(disallowed.domains.metro?.delta.addedBytes).toBe(3); + + const allowed = planSave( + paths, + before, + { mode: 'default-branch', isDefaultBranch: true }, + inputs() + ); + expect(allowed.domains.metro?.shouldSave).toBe(true); + }); + + it('never saves when policy mode is "never"', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + const before = snapshot(paths); + fs.writeFileSync(path.join(paths.metroTransform, 'a.txt'), 'aaa'); + + const plan = planSave( + paths, + before, + { mode: 'never', isDefaultBranch: true }, + inputs() + ); + + expect(plan.domains.metro?.shouldSave).toBe(false); + }); + + it('counts a removed file in the delta', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + fs.writeFileSync(path.join(paths.metroTransform, 'a.txt'), 'aaa'); + const before = snapshot(paths); + + fs.rmSync(path.join(paths.metroTransform, 'a.txt')); + + const plan = planSave( + paths, + before, + { mode: 'always', isDefaultBranch: true }, + inputs() + ); + + expect(plan.domains.metro?.delta.removedEntries).toBe(1); + expect(plan.domains.metro?.shouldSave).toBe(true); + }); + + it('does not treat an mtime-only change as an added entry or change the content hash', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + const filePath = path.join(paths.metroTransform, 'a.txt'); + fs.writeFileSync(filePath, 'aaa'); + const before = snapshot(paths); + + const beforeSave = planSave( + paths, + before, + { mode: 'always', isDefaultBranch: true }, + inputs() + ); + + // Touch the file's mtime without changing its size/content. + const future = new Date(Date.now() + 10_000); + fs.utimesSync(filePath, future, future); + + const afterSave = planSave( + paths, + before, + { mode: 'always', isDefaultBranch: true }, + inputs() + ); + + expect(afterSave.domains.metro?.delta.addedEntries).toBe(0); + expect(afterSave.domains.metro?.shouldSave).toBe(false); + expect(afterSave.domains.metro?.saveKey).toBe( + beforeSave.domains.metro?.saveKey + ); + }); + + it('produces a deterministic saveKey across independent planSave calls given the same on-disk state', () => { + const paths = createHarnessCachePaths(projectRoot); + fs.mkdirSync(paths.metroTransform, { recursive: true }); + fs.writeFileSync(path.join(paths.metroTransform, 'a.txt'), 'aaa'); + const before: CacheSnapshot = {}; + + const first = planSave( + paths, + before, + { mode: 'always', isDefaultBranch: true }, + inputs() + ); + const second = planSave( + paths, + before, + { mode: 'always', isDefaultBranch: true }, + inputs() + ); + + expect(first.domains.metro?.saveKey).toBe(second.domains.metro?.saveKey); + }); +}); + +describe('writeKeysFile', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-')); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + it('writes keys.json under the cache root, outside domain subdirectories', () => { + const paths = createHarnessCachePaths(projectRoot); + const plan = planRestore(paths, { + metro: { os: 'linux', staticInputs: { a: '1' } }, + }); + + writeKeysFile(paths, plan); + + const filePath = path.join(paths.root, 'keys.json'); + expect(fs.existsSync(filePath)).toBe(true); + expect(filePath.startsWith(paths.metroTransform)).toBe(false); + expect(filePath.startsWith(paths.metroFileMap)).toBe(false); + + const written = JSON.parse(fs.readFileSync(filePath, 'utf8')); + expect(written.version).toBe(1); + }); + + it('swallows errors instead of throwing when the cache root cannot be created', () => { + const blockedRoot = path.join(projectRoot, 'blocked-file', 'nested'); + fs.writeFileSync(path.join(projectRoot, 'blocked-file'), 'not a directory'); + const paths = createHarnessCachePaths(blockedRoot); + const plan = planRestore(paths, { + metro: { os: 'linux', staticInputs: { a: '1' } }, + }); + + expect(() => writeKeysFile(paths, plan)).not.toThrow(); + expect(fs.existsSync(path.join(paths.root, 'keys.json'))).toBe(false); + }); +}); diff --git a/packages/cache/src/domains/metro.ts b/packages/cache/src/domains/metro.ts new file mode 100644 index 00000000..e361873f --- /dev/null +++ b/packages/cache/src/domains/metro.ts @@ -0,0 +1,103 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { logger } from '@react-native-harness/tools'; + +const cacheLogger = logger.child('cache'); + +// Directories that never contain a repo's own lockfiles/config, only vendored +// or generated copies of them (e.g. a dependency's own package-lock.json +// under node_modules). Skipping these is the correctness fix over today's +// `hashFiles('**/pnpm-lock.yaml', ...)` glob, which matches inside +// node_modules too. +const SKIPPED_DIRNAMES = new Set(['node_modules', '.git', 'dist', 'build']); + +export const METRO_KEY_FILENAMES: readonly string[] = [ + 'bun.lock', + 'bun.lockb', + 'package-lock.json', + 'npm-shrinkwrap.json', + 'pnpm-lock.yaml', + 'yarn.lock', + 'metro.config.js', + 'metro.config.cjs', + 'metro.config.mjs', + 'metro.config.ts', + 'babel.config.js', + 'babel.config.cjs', + 'babel.config.mjs', + 'babel.config.ts', + 'babel.config.json', +]; + +const METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); + +interface KeyFileMatch { + relativePath: string; + content: Buffer; +} + +const collectKeyFiles = ( + repoRoot: string, + directory: string, + matches: KeyFileMatch[] +): void => { + for (const dirent of fs.readdirSync(directory, { withFileTypes: true })) { + if (dirent.isDirectory()) { + if (SKIPPED_DIRNAMES.has(dirent.name)) { + continue; + } + collectKeyFiles(repoRoot, path.join(directory, dirent.name), matches); + } else if (dirent.isFile() && METRO_KEY_FILENAME_SET.has(dirent.name)) { + const fullPath = path.join(directory, dirent.name); + matches.push({ + relativePath: path + .relative(repoRoot, fullPath) + .split(path.sep) + .join('/'), + content: fs.readFileSync(fullPath), + }); + } + } +}; + +const hashKeyFiles = (matches: KeyFileMatch[]): string => { + const hash = createHash('sha256'); + + for (const match of matches + .slice() + .sort((a, b) => (a.relativePath < b.relativePath ? -1 : 1))) { + hash.update(match.relativePath); + hash.update('\0'); + hash.update(match.content); + hash.update('\0'); + } + + return hash.digest('hex'); +}; + +export const computeMetroStaticInputs = (options: { + repoRoot: string; + bundlerMetroVersion: string; + salt?: string; +}): Record => { + try { + const matches: KeyFileMatch[] = []; + collectKeyFiles(options.repoRoot, options.repoRoot, matches); + + return { + lockfileHash: hashKeyFiles(matches), + bundlerMetroVersion: options.bundlerMetroVersion, + ...(options.salt ? { salt: options.salt } : {}), + }; + } catch (error) { + cacheLogger.warn( + `Failed to compute Metro cache key inputs for "${options.repoRoot}". Falling back to an always-miss key.`, + error + ); + return { + lockfileHash: 'unavailable', + bundlerMetroVersion: options.bundlerMetroVersion, + }; + } +}; diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts index e511d23f..6e9cff23 100644 --- a/packages/cache/src/index.ts +++ b/packages/cache/src/index.ts @@ -6,14 +6,51 @@ import { type CacheDomainId, type HarnessCachePaths, } from './paths.js'; +import { + planRestore, + planSave, + snapshot, + writeKeysFile, + type CacheKeyInputs, + type CacheSnapshot, + type CacheSnapshotEntry, + type DomainRestorePlan, + type DomainSavePlan, + type RestorePlan, + type SavePlan, + type SavePolicy, +} from './plan.js'; import { isDomainWarm } from './warmth.js'; +import { computeMetroStaticInputs, METRO_KEY_FILENAMES } from './domains/metro.js'; + +export { computeMetroStaticInputs, METRO_KEY_FILENAMES }; export type { CacheDomainId, HarnessCachePaths }; +export type { + CacheKeyInputs, + CacheSnapshot, + CacheSnapshotEntry, + DomainRestorePlan, + DomainSavePlan, + RestorePlan, + SavePlan, + SavePolicy, +}; export interface HarnessCache { readonly paths: HarnessCachePaths; isWarm(domain: CacheDomainId): boolean; ensureDomainDirectories(domain: CacheDomainId): void; + planRestore( + inputs: Partial> + ): RestorePlan; + snapshot(): CacheSnapshot; + planSave( + before: CacheSnapshot, + policy: SavePolicy, + inputs: Partial> + ): SavePlan; + writeKeysFile(plan: RestorePlan | SavePlan): void; } const cacheLogger = logger.child('cache'); @@ -38,5 +75,10 @@ export const createHarnessCache = (options: { } } }, + planRestore: (inputs) => planRestore(paths, inputs), + snapshot: () => snapshot(paths), + planSave: (before, policy, inputs) => + planSave(paths, before, policy, inputs), + writeKeysFile: (plan) => writeKeysFile(paths, plan), }; }; diff --git a/packages/cache/src/plan.ts b/packages/cache/src/plan.ts new file mode 100644 index 00000000..c1eda032 --- /dev/null +++ b/packages/cache/src/plan.ts @@ -0,0 +1,285 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { logger } from '@react-native-harness/tools'; +import { getDomainDirectories } from './paths.js'; +import type { CacheDomainId, HarnessCachePaths } from './paths.js'; + +const cacheLogger = logger.child('cache'); + +const KEYS_FILENAME = 'keys.json'; +const HASH_LENGTH = 16; + +export interface CacheKeyInputs { + os: string; + staticInputs: Record; +} + +export interface DomainRestorePlan { + paths: string[]; + restoreKey: string; + restorePrefixes: string[]; + accretive: boolean; +} + +export interface RestorePlan { + version: 1; + domains: Partial>; +} + +export interface CacheSnapshotEntry { + path: string; + sizeBytes: number; + mtimeMs: number; +} + +export type CacheSnapshot = Partial>; + +export interface SavePolicy { + mode: 'default-branch' | 'always' | 'never'; + isDefaultBranch: boolean; +} + +export interface DomainSavePlan { + shouldSave: boolean; + saveKey: string; + delta: { + addedEntries: number; + removedEntries: number; + addedBytes: number; + }; +} + +export interface SavePlan { + version: 1; + domains: Partial>; +} + +const ALL_DOMAINS: readonly CacheDomainId[] = ['metro']; + +/** + * Accretive domains only ever grow (transform/file-map caches keyed by + * content hash) so a save is always safe to key off the static hash plus a + * content hash, and prior saves can be found via a prefix match on the + * static hash. Non-accretive domains would need an exact key match instead. + * A domain not yet handled here defaults to non-accretive (exact-match), + * matching the safe-fallback style of `getDomainDirectories`/ + * `getWarmthDirectory`: prefix-matching into an unknown domain's saves would + * not be a neutral no-op, so the conservative default is to require an exact + * key match rather than silently opt a future domain into accretive lookup. + */ +const isAccretiveDomain = (domain: CacheDomainId): boolean => { + switch (domain) { + case 'metro': + return true; + default: + return false; + } +}; + +const hashSortedEntries = (entries: Array<[string, string]>): string => + createHash('sha256') + .update( + entries + .slice() + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([key, value]) => `${key}=${value}`) + .join('\n') + ) + .digest('hex') + .slice(0, HASH_LENGTH); + +const buildStaticHashKey = ( + domain: CacheDomainId, + inputs: CacheKeyInputs +): string => { + const hash = hashSortedEntries(Object.entries(inputs.staticInputs)); + return `harness-${domain}-${inputs.os}-${hash}`; +}; + +const buildContentHash = (entries: CacheSnapshotEntry[]): string => + hashSortedEntries( + entries.map((entry) => [entry.path, String(entry.sizeBytes)]) + ); + +const walkDirectory = ( + root: string, + directory: string, + entries: CacheSnapshotEntry[] +): void => { + let dirents: fs.Dirent[]; + + try { + dirents = fs.readdirSync(directory, { withFileTypes: true }); + } catch { + // Missing/unreadable directory is treated as empty rather than an error. + return; + } + + for (const dirent of dirents) { + const fullPath = path.join(directory, dirent.name); + + if (dirent.isDirectory()) { + walkDirectory(root, fullPath, entries); + } else if (dirent.isFile()) { + const stat = fs.statSync(fullPath); + entries.push({ + path: path.relative(root, fullPath), + sizeBytes: stat.size, + mtimeMs: stat.mtimeMs, + }); + } + } +}; + +export const planRestore = ( + paths: HarnessCachePaths, + inputs: Partial> +): RestorePlan => { + const domains: RestorePlan['domains'] = {}; + + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + + const restoreKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + + domains[domain] = { + paths: [...getDomainDirectories(domain, paths)], + restoreKey, + restorePrefixes: accretive ? [`${restoreKey}-`] : [], + accretive, + }; + } + + return { version: 1, domains }; +}; + +/** + * Synchronous snapshot of on-disk cache contents, keyed by domain. Never + * throws: a domain whose directories cannot be read is reported as empty. + */ +export const snapshot = (paths: HarnessCachePaths): CacheSnapshot => { + const result: CacheSnapshot = {}; + + for (const domain of ALL_DOMAINS) { + try { + const entries: CacheSnapshotEntry[] = []; + for (const directory of getDomainDirectories(domain, paths)) { + walkDirectory(paths.root, directory, entries); + } + result[domain] = entries; + } catch (error) { + cacheLogger.warn( + `Failed to snapshot cache domain "${domain}". Treating as empty.`, + error + ); + result[domain] = []; + } + } + + return result; +}; + +const diffSnapshots = ( + before: CacheSnapshotEntry[], + after: CacheSnapshotEntry[] +): DomainSavePlan['delta'] => { + const beforeByPath = new Map(before.map((entry) => [entry.path, entry])); + const afterByPath = new Map(after.map((entry) => [entry.path, entry])); + + let addedEntries = 0; + let removedEntries = 0; + let addedBytes = 0; + + for (const [entryPath, afterEntry] of afterByPath) { + const beforeEntry = beforeByPath.get(entryPath); + if (!beforeEntry || beforeEntry.sizeBytes !== afterEntry.sizeBytes) { + addedEntries += 1; + addedBytes += afterEntry.sizeBytes; + } + } + + for (const entryPath of beforeByPath.keys()) { + if (!afterByPath.has(entryPath)) { + removedEntries += 1; + } + } + + return { addedEntries, removedEntries, addedBytes }; +}; + +const shouldSaveForPolicy = ( + policy: SavePolicy, + delta: DomainSavePlan['delta'] +): boolean => { + if (delta.addedEntries + delta.removedEntries === 0) { + return false; + } + + switch (policy.mode) { + case 'always': + return true; + case 'default-branch': + return policy.isDefaultBranch; + case 'never': + return false; + } +}; + +export const planSave = ( + paths: HarnessCachePaths, + before: CacheSnapshot, + policy: SavePolicy, + inputs: Partial> +): SavePlan => { + const after = snapshot(paths); + const domains: SavePlan['domains'] = {}; + + for (const domain of ALL_DOMAINS) { + const domainInputs = inputs[domain]; + if (!domainInputs) { + continue; + } + + const beforeEntries = before[domain] ?? []; + const afterEntries = after[domain] ?? []; + const delta = diffSnapshots(beforeEntries, afterEntries); + const staticHashKey = buildStaticHashKey(domain, domainInputs); + const accretive = isAccretiveDomain(domain); + + domains[domain] = { + shouldSave: shouldSaveForPolicy(policy, delta), + saveKey: accretive + ? `${staticHashKey}-${buildContentHash(afterEntries)}` + : staticHashKey, + delta, + }; + } + + return { version: 1, domains }; +}; + +/** + * Written to `/keys.json`, outside any domain's cached + * subdirectories, so it is never itself part of what gets restored/saved. + * It is a secondary/debug artifact; the actual data flow between CI steps + * goes through GITHUB_OUTPUT, not by re-reading this file. Cache I/O must + * never fail a run, so write failures are logged and swallowed. + */ +export const writeKeysFile = ( + paths: HarnessCachePaths, + plan: RestorePlan | SavePlan +): void => { + const filePath = path.join(paths.root, KEYS_FILENAME); + + try { + fs.mkdirSync(paths.root, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(plan, null, 2)); + } catch (error) { + cacheLogger.warn(`Failed to write cache keys file "${filePath}".`, error); + } +}; diff --git a/packages/github-action/README.md b/packages/github-action/README.md index b10b9f14..8c4ff783 100644 --- a/packages/github-action/README.md +++ b/packages/github-action/README.md @@ -32,8 +32,9 @@ The action reads your `rn-harness.config.mjs` file, resolves the `runner` you pa - `cacheAvd` (optional, Android only): Whether to cache the Android Virtual Device snapshot. Defaults to `true` - `preRunHook` (optional): Inline shell script run in `bash` immediately before Harness starts - `afterRunHook` (optional): Inline shell script run in `bash` immediately after Harness finishes and before artifact upload +- `cacheSavePolicy` (optional): When to save a new Metro cache entry -- `default-branch` (default), `always`, or `never` - Crash artifacts persisted to `.harness/crash-reports/` are uploaded automatically when present -- Metro cache persisted to `.harness/cache/metro/` and `.harness/cache/metro-file-map/` is restored and saved automatically when present +- Metro cache persisted to `.harness/cache/metro/` and `.harness/cache/metro-file-map/` is restored and saved automatically when present. The cache key is computed by `@react-native-harness/cache` from your lockfile(s), Metro/Babel config, the installed `@react-native-harness/bundler-metro` version, and your Harness config's `cache.version` salt -- a new entry is only saved when the run's cache content actually changed and `cacheSavePolicy` allows it ## Behavior diff --git a/packages/github-action/eslint.config.mjs b/packages/github-action/eslint.config.mjs index dc4ddc31..0f029721 100644 --- a/packages/github-action/eslint.config.mjs +++ b/packages/github-action/eslint.config.mjs @@ -12,6 +12,14 @@ export default [ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}', '{projectRoot}/tsup.config.{js,cjs,mjs,ts,cts,mts}', ], + ignoredDependencies: [ + 'vite', + 'vitest', + // Referenced only as a require.resolve() target to read the + // consuming project's own installed version at runtime, not + // imported/bundled by this package. + '@react-native-harness/bundler-metro', + ], }, ], }, diff --git a/packages/github-action/package.json b/packages/github-action/package.json index 4d2b87c2..85b6bc0e 100644 --- a/packages/github-action/package.json +++ b/packages/github-action/package.json @@ -11,6 +11,7 @@ "build": "tsup" }, "dependencies": { + "@react-native-harness/cache": "workspace:*", "@react-native-harness/config": "workspace:*", "@react-native-harness/platform-android": "workspace:*" }, diff --git a/packages/github-action/src/action.yml b/packages/github-action/src/action.yml index 09cf8899..655608b7 100644 --- a/packages/github-action/src/action.yml +++ b/packages/github-action/src/action.yml @@ -43,6 +43,11 @@ inputs: required: false type: string default: '' + cacheSavePolicy: + description: When to save a new Metro cache entry ('default-branch', 'always', or 'never') + required: false + type: string + default: 'default-branch' runs: using: 'composite' steps: @@ -64,21 +69,41 @@ runs: echo "Please provide the path to the built app (.apk for Android, .app for iOS)" exit 1 fi - - name: Metro bundler cache (.harness/cache/metro) - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + - name: Plan Metro cache restore + id: plan-metro-restore + shell: bash + env: + INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + run: | + node ${{ github.action_path }}/actions/shared/plan-restore.cjs + - name: Restore Metro cache (.harness/cache/metro) + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro-file-map - key: ${{ runner.os }}-metro-cache-v2-${{ hashFiles('**/bun.lock', '**/bun.lockb', '**/package-lock.json', '**/npm-shrinkwrap.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/metro.config.js', '**/metro.config.cjs', '**/metro.config.mjs', '**/metro.config.ts', '**/babel.config.js', '**/babel.config.cjs', '**/babel.config.mjs', '**/babel.config.ts', '**/babel.config.json') }} - restore-keys: | - ${{ runner.os }}-metro-cache-v2- + key: ${{ steps.plan-metro-restore.outputs.metroRestoreKey }} + # The '\n' here is a literal newline embedded via YAML double-quote + # escaping (not a GitHub Actions expression escape -- expression + # string literals don't interpret backslash sequences), so join() + # actually separates entries onto their own lines, as actions/cache + # requires for restore-keys. + restore-keys: "${{ join(fromJson(steps.plan-metro-restore.outputs.metroRestorePrefixes), '\n') }}" + # Must run after "Restore Metro cache" so this reflects what was actually + # restored (empty on a cache miss), not the always-empty pre-restore state. + - name: Snapshot Metro cache + id: snapshot-metro-restore + shell: bash + env: + INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + run: | + node ${{ github.action_path }}/actions/shared/snapshot-metro.cjs - name: Restore Harness cache id: cache-harness-restore if: fromJson(steps.load-config.outputs.config).platformId == 'ios' uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache + path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/xctest-agent-simulator-* key: harness-ios-${{ runner.os }}-${{ hashFiles(format('{0}/.harness/cache/**/cache.json', steps.load-config.outputs.projectRoot)) }} restore-keys: | harness-ios-${{ runner.os }}- @@ -242,8 +267,27 @@ runs: if: ${{ always() && fromJson(steps.load-config.outputs.config).platformId == 'ios' && steps.cache-harness-restore.outputs.cache-hit != 'true' }} uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: - path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache + path: ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/xctest-agent-simulator-* key: ${{ steps.cache-harness-restore.outputs.cache-primary-key }} + - name: Plan Metro cache save + id: plan-metro-save + if: always() + shell: bash + env: + INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + INPUT_METRO_SNAPSHOT: ${{ steps.snapshot-metro-restore.outputs.metroSnapshot }} + INPUT_CACHESAVEPOLICY: ${{ inputs.cacheSavePolicy }} + IS_DEFAULT_BRANCH: ${{ github.ref_name == github.event.repository.default_branch }} + run: | + node ${{ github.action_path }}/actions/shared/plan-save.cjs + - name: Save Metro cache + if: always() && steps.plan-metro-save.outputs.metroShouldSave == 'true' + uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro + ${{ steps.load-config.outputs.projectRoot }}/.harness/cache/metro-file-map + key: ${{ steps.plan-metro-save.outputs.metroSaveKey }} - name: Upload crash report artifacts if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts b/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts new file mode 100644 index 00000000..1e38f511 --- /dev/null +++ b/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts @@ -0,0 +1,77 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + resolveBundlerMetroVersion, + resolveRepoRoot, +} from '../metro-cache-inputs.js'; + +describe('resolveRepoRoot', () => { + let root: string; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-repo-root-')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it('walks up to the nearest ancestor containing a .git directory', () => { + fs.mkdirSync(path.join(root, '.git')); + const projectRoot = path.join(root, 'apps', 'mobile'); + fs.mkdirSync(projectRoot, { recursive: true }); + + expect(resolveRepoRoot(projectRoot)).toBe(root); + }); + + it('falls back to projectRoot when no .git directory is found', () => { + const projectRoot = path.join(root, 'no-git-here'); + fs.mkdirSync(projectRoot, { recursive: true }); + + expect(resolveRepoRoot(projectRoot)).toBe(projectRoot); + }); +}); + +describe('resolveBundlerMetroVersion', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'gha-bundler-metro-version-') + ); + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('resolves the version from the consuming project\'s installed package', () => { + const packageDir = path.join( + projectRoot, + 'node_modules', + '@react-native-harness', + 'bundler-metro' + ); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync( + path.join(packageDir, 'package.json'), + JSON.stringify({ + name: '@react-native-harness/bundler-metro', + version: '9.9.9', + main: 'index.js', + }) + ); + fs.writeFileSync(path.join(packageDir, 'index.js'), 'module.exports = {};'); + + expect(resolveBundlerMetroVersion(projectRoot)).toBe('9.9.9'); + }); + + it('falls back to "unknown" and warns when the package cannot be resolved', () => { + expect(resolveBundlerMetroVersion(projectRoot)).toBe('unknown'); + expect(console.warn).toHaveBeenCalled(); + }); +}); diff --git a/packages/github-action/src/shared/__tests__/plan-restore.test.ts b/packages/github-action/src/shared/__tests__/plan-restore.test.ts new file mode 100644 index 00000000..e2c89d7f --- /dev/null +++ b/packages/github-action/src/shared/__tests__/plan-restore.test.ts @@ -0,0 +1,123 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + getConfig: vi.fn(), + createHarnessCache: vi.fn(), + computeMetroStaticInputs: vi.fn(), +})); + +vi.mock('@react-native-harness/config', () => ({ + getConfig: mocks.getConfig, +})); + +// This package's job is wiring, not re-testing @react-native-harness/cache's +// own logic, so its exports are mocked rather than exercised for real. +vi.mock('@react-native-harness/cache', () => ({ + createHarnessCache: mocks.createHarnessCache, + computeMetroStaticInputs: mocks.computeMetroStaticInputs, +})); + +describe('plan-restore', () => { + let projectRoot: string; + let githubOutputPath: string; + let planRestoreMock: ReturnType; + let writeKeysFileMock: ReturnType; + + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-plan-restore-')); + githubOutputPath = path.join(projectRoot, 'github-output.txt'); + fs.writeFileSync(githubOutputPath, ''); + + process.env.INPUT_PROJECTROOT = projectRoot; + process.env.GITHUB_OUTPUT = githubOutputPath; + process.env.RUNNER_OS = 'Linux'; + + mocks.getConfig.mockResolvedValue({ + config: { cache: { version: 'salt-1' } }, + projectRoot, + }); + mocks.computeMetroStaticInputs.mockReturnValue({ lockfileHash: 'abc' }); + + planRestoreMock = vi.fn().mockReturnValue({ + version: 1, + domains: { + metro: { + paths: ['/tmp/metro', '/tmp/metro-file-map'], + restoreKey: 'harness-metro-linux-abc', + restorePrefixes: ['harness-metro-linux-abc-'], + accretive: true, + }, + }, + }); + writeKeysFileMock = vi.fn(); + + mocks.createHarnessCache.mockReturnValue({ + planRestore: planRestoreMock, + writeKeysFile: writeKeysFileMock, + }); + + vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + vi.spyOn(console, 'error').mockImplementation(() => undefined); + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + vi.spyOn(console, 'info').mockImplementation(() => undefined); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + delete process.env.INPUT_PROJECTROOT; + delete process.env.GITHUB_OUTPUT; + delete process.env.RUNNER_OS; + vi.restoreAllMocks(); + }); + + it('writes metroRestoreKey and metroRestorePrefixes (as JSON) to GITHUB_OUTPUT', async () => { + await import('../plan-restore.js'); + + await vi.waitFor(() => { + expect(writeKeysFileMock).toHaveBeenCalled(); + }); + + const output = fs.readFileSync(githubOutputPath, 'utf8'); + expect(output).toContain('metroRestoreKey=harness-metro-linux-abc\n'); + expect(output).toContain( + 'metroRestorePrefixes=["harness-metro-linux-abc-"]\n' + ); + // The pre-run snapshot is taken by the separate snapshot-metro step, + // which runs after the actual cache restore -- not here. + expect(output).not.toContain('metroSnapshot'); + }); + + it('calls planRestore with the os and staticInputs assembled from the Metro key recipe', async () => { + await import('../plan-restore.js'); + + await vi.waitFor(() => { + expect(planRestoreMock).toHaveBeenCalled(); + }); + + expect(planRestoreMock).toHaveBeenCalledWith({ + metro: { os: 'Linux', staticInputs: { lockfileHash: 'abc' } }, + }); + expect(mocks.computeMetroStaticInputs).toHaveBeenCalledWith( + expect.objectContaining({ salt: 'salt-1' }) + ); + }); + + it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { + delete process.env.GITHUB_OUTPUT; + + await import('../plan-restore.js'); + + await vi.waitFor(() => { + expect(process.exit).toHaveBeenCalledWith(1); + }); + expect(console.error).toHaveBeenCalledWith( + 'GITHUB_OUTPUT environment variable is not set' + ); + }); +}); diff --git a/packages/github-action/src/shared/__tests__/plan-save.test.ts b/packages/github-action/src/shared/__tests__/plan-save.test.ts new file mode 100644 index 00000000..839d6438 --- /dev/null +++ b/packages/github-action/src/shared/__tests__/plan-save.test.ts @@ -0,0 +1,152 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + getConfig: vi.fn(), + createHarnessCache: vi.fn(), + computeMetroStaticInputs: vi.fn(), +})); + +vi.mock('@react-native-harness/config', () => ({ + getConfig: mocks.getConfig, +})); + +vi.mock('@react-native-harness/cache', () => ({ + createHarnessCache: mocks.createHarnessCache, + computeMetroStaticInputs: mocks.computeMetroStaticInputs, +})); + +describe('plan-save', () => { + let projectRoot: string; + let githubOutputPath: string; + let planSaveMock: ReturnType; + let writeKeysFileMock: ReturnType; + + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-plan-save-')); + githubOutputPath = path.join(projectRoot, 'github-output.txt'); + fs.writeFileSync(githubOutputPath, ''); + + process.env.INPUT_PROJECTROOT = projectRoot; + process.env.GITHUB_OUTPUT = githubOutputPath; + process.env.RUNNER_OS = 'Linux'; + process.env.INPUT_METRO_SNAPSHOT = JSON.stringify({ metro: [] }); + process.env.IS_DEFAULT_BRANCH = 'true'; + process.env.INPUT_CACHESAVEPOLICY = 'default-branch'; + + mocks.getConfig.mockResolvedValue({ + config: { cache: { version: 'salt-1' } }, + projectRoot, + }); + mocks.computeMetroStaticInputs.mockReturnValue({ lockfileHash: 'abc' }); + + planSaveMock = vi.fn().mockReturnValue({ + version: 1, + domains: { + metro: { + shouldSave: true, + saveKey: 'harness-metro-linux-abc-contenthash', + delta: { addedEntries: 1, removedEntries: 0, addedBytes: 10 }, + }, + }, + }); + writeKeysFileMock = vi.fn(); + + mocks.createHarnessCache.mockReturnValue({ + planSave: planSaveMock, + writeKeysFile: writeKeysFileMock, + }); + + vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + vi.spyOn(console, 'error').mockImplementation(() => undefined); + vi.spyOn(console, 'warn').mockImplementation(() => undefined); + vi.spyOn(console, 'info').mockImplementation(() => undefined); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + delete process.env.INPUT_PROJECTROOT; + delete process.env.GITHUB_OUTPUT; + delete process.env.RUNNER_OS; + delete process.env.INPUT_METRO_SNAPSHOT; + delete process.env.IS_DEFAULT_BRANCH; + delete process.env.INPUT_CACHESAVEPOLICY; + vi.restoreAllMocks(); + }); + + it('writes metroShouldSave and metroSaveKey to GITHUB_OUTPUT', async () => { + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(writeKeysFileMock).toHaveBeenCalled(); + }); + + const output = fs.readFileSync(githubOutputPath, 'utf8'); + expect(output).toContain('metroShouldSave=true\n'); + expect(output).toContain( + 'metroSaveKey=harness-metro-linux-abc-contenthash\n' + ); + }); + + it('parses the metroSnapshot input and builds the SavePolicy from env/input', async () => { + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(planSaveMock).toHaveBeenCalled(); + }); + + expect(planSaveMock).toHaveBeenCalledWith( + { metro: [] }, + { mode: 'default-branch', isDefaultBranch: true }, + { metro: { os: 'Linux', staticInputs: { lockfileHash: 'abc' } } } + ); + }); + + it('treats an unparseable metroSnapshot input as an empty snapshot instead of throwing', async () => { + process.env.INPUT_METRO_SNAPSHOT = 'not json'; + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(planSaveMock).toHaveBeenCalled(); + }); + + expect(planSaveMock).toHaveBeenCalledWith( + {}, + expect.anything(), + expect.anything() + ); + expect(console.warn).toHaveBeenCalled(); + }); + + it('defaults an unrecognized cacheSavePolicy input to "default-branch"', async () => { + process.env.INPUT_CACHESAVEPOLICY = 'bogus'; + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(planSaveMock).toHaveBeenCalled(); + }); + + expect(planSaveMock).toHaveBeenCalledWith( + expect.anything(), + { mode: 'default-branch', isDefaultBranch: true }, + expect.anything() + ); + }); + + it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { + delete process.env.GITHUB_OUTPUT; + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(process.exit).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts b/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts new file mode 100644 index 00000000..adf4f09e --- /dev/null +++ b/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts @@ -0,0 +1,82 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + getConfig: vi.fn(), + createHarnessCache: vi.fn(), +})); + +vi.mock('@react-native-harness/config', () => ({ + getConfig: mocks.getConfig, +})); + +vi.mock('@react-native-harness/cache', () => ({ + createHarnessCache: mocks.createHarnessCache, +})); + +describe('snapshot-metro', () => { + let projectRoot: string; + let githubOutputPath: string; + let snapshotMock: ReturnType; + + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-snapshot-metro-')); + githubOutputPath = path.join(projectRoot, 'github-output.txt'); + fs.writeFileSync(githubOutputPath, ''); + + process.env.INPUT_PROJECTROOT = projectRoot; + process.env.GITHUB_OUTPUT = githubOutputPath; + + mocks.getConfig.mockResolvedValue({ + config: {}, + projectRoot, + }); + + snapshotMock = vi.fn().mockReturnValue({ + metro: [{ path: 'metro/a', sizeBytes: 3, mtimeMs: 1 }], + }); + + mocks.createHarnessCache.mockReturnValue({ + snapshot: snapshotMock, + }); + + vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + vi.spyOn(console, 'error').mockImplementation(() => undefined); + vi.spyOn(console, 'info').mockImplementation(() => undefined); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + delete process.env.INPUT_PROJECTROOT; + delete process.env.GITHUB_OUTPUT; + vi.restoreAllMocks(); + }); + + it('writes the current cache snapshot to GITHUB_OUTPUT as metroSnapshot', async () => { + await import('../snapshot-metro.js'); + + await vi.waitFor(() => { + expect(snapshotMock).toHaveBeenCalled(); + }); + + const output = fs.readFileSync(githubOutputPath, 'utf8'); + expect(output).toContain( + 'metroSnapshot={"metro":[{"path":"metro/a","sizeBytes":3,"mtimeMs":1}]}\n' + ); + }); + + it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { + delete process.env.GITHUB_OUTPUT; + + await import('../snapshot-metro.js'); + + await vi.waitFor(() => { + expect(process.exit).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/packages/github-action/src/shared/metro-cache-inputs.ts b/packages/github-action/src/shared/metro-cache-inputs.ts new file mode 100644 index 00000000..5623a3cd --- /dev/null +++ b/packages/github-action/src/shared/metro-cache-inputs.ts @@ -0,0 +1,75 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { computeMetroStaticInputs } from '@react-native-harness/cache'; + +const FALLBACK_BUNDLER_METRO_VERSION = 'unknown'; + +/** + * Lockfiles in a monorepo often live above the Harness projectRoot, so the + * Metro key recipe walk anchors at the git repository root when discoverable + * (mirroring how action.yml's `hashFiles('**\/pnpm-lock.yaml', ...)` today + * searches from the workflow's checkout root, not from projectRoot), falling + * back to projectRoot itself for shallow/sparse checkouts with no `.git`. + */ +export const resolveRepoRoot = (projectRoot: string): string => { + let dir = projectRoot; + + while (true) { + if (fs.existsSync(path.join(dir, '.git'))) { + return dir; + } + + const parent = path.dirname(dir); + if (parent === dir) { + return projectRoot; + } + + dir = parent; + } +}; + +/** + * Resolved from the consuming project's own installed copy of + * `@react-native-harness/bundler-metro`, since that's the actual Metro + * integration whose behavior determines cache validity for that project. + * Never throws: a broken/missing resolution degrades to a stable + * placeholder rather than aborting the CI step. + */ +export const resolveBundlerMetroVersion = (projectRoot: string): string => { + try { + const packageJsonPath = require.resolve( + '@react-native-harness/bundler-metro/package.json', + { paths: [projectRoot] } + ); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + if (typeof packageJson.version !== 'string') { + throw new Error('"version" field is missing or not a string'); + } + + return packageJson.version; + } catch (error) { + console.warn( + `Failed to resolve @react-native-harness/bundler-metro's version from "${projectRoot}". ` + + `Falling back to "${FALLBACK_BUNDLER_METRO_VERSION}".`, + error + ); + return FALLBACK_BUNDLER_METRO_VERSION; + } +}; + +/** + * Shared by plan-restore and plan-save so both steps derive the exact same + * staticInputs from the same recipe. computeMetroStaticInputs is + * deterministic given the same inputs, so recomputing it in the later + * plan-save step (rather than threading it through GITHUB_OUTPUT) is safe. + */ +export const resolveMetroStaticInputs = (options: { + projectRoot: string; + cacheVersionSalt?: string; +}): Record => + computeMetroStaticInputs({ + repoRoot: resolveRepoRoot(options.projectRoot), + bundlerMetroVersion: resolveBundlerMetroVersion(options.projectRoot), + salt: options.cacheVersionSalt, + }); diff --git a/packages/github-action/src/shared/plan-restore.ts b/packages/github-action/src/shared/plan-restore.ts new file mode 100644 index 00000000..67691bef --- /dev/null +++ b/packages/github-action/src/shared/plan-restore.ts @@ -0,0 +1,69 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createHarnessCache } from '@react-native-harness/cache'; +import { getConfig } from '@react-native-harness/config'; +import { resolveMetroStaticInputs } from './metro-cache-inputs.js'; + +const run = async (): Promise => { + try { + const projectRootInput = process.env.INPUT_PROJECTROOT; + + const projectRoot = projectRootInput + ? path.resolve(projectRootInput) + : process.cwd(); + + console.info(`Planning Metro cache restore for: ${projectRoot}`); + + const { config, projectRoot: resolvedProjectRoot } = await getConfig( + projectRoot + ); + + const githubOutput = process.env.GITHUB_OUTPUT; + if (!githubOutput) { + throw new Error('GITHUB_OUTPUT environment variable is not set'); + } + + const staticInputs = resolveMetroStaticInputs({ + projectRoot: resolvedProjectRoot, + cacheVersionSalt: config.cache?.version, + }); + + const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); + const os = process.env.RUNNER_OS ?? process.platform; + + const restorePlan = cache.planRestore({ + metro: { os, staticInputs }, + }); + cache.writeKeysFile(restorePlan); + + const metroPlan = restorePlan.domains.metro; + + // Structured values are passed through GITHUB_OUTPUT as single-line JSON + // strings, matching the `config=${JSON.stringify(...)}` convention + // already used by shared/index.ts (parsed on the YAML side via + // `fromJson(...)`). + // + // The pre-run cache snapshot is deliberately NOT taken here: this step + // runs before the actual `actions/cache/restore` step, so the cache + // directories are still empty at this point. It's captured by a + // separate snapshot-metro step that runs after the restore action + // instead (see snapshot-metro.ts). + const output = + `metroRestoreKey=${metroPlan?.restoreKey ?? ''}\n` + + `metroRestorePrefixes=${JSON.stringify( + metroPlan?.restorePrefixes ?? [] + )}\n`; + + fs.appendFileSync(githubOutput, output); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } else { + console.error('Failed to plan Metro cache restore'); + } + + process.exit(1); + } +}; + +run(); diff --git a/packages/github-action/src/shared/plan-save.ts b/packages/github-action/src/shared/plan-save.ts new file mode 100644 index 00000000..3fa4e01b --- /dev/null +++ b/packages/github-action/src/shared/plan-save.ts @@ -0,0 +1,96 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { + createHarnessCache, + type CacheSnapshot, + type SavePolicy, +} from '@react-native-harness/cache'; +import { getConfig } from '@react-native-harness/config'; +import { resolveMetroStaticInputs } from './metro-cache-inputs.js'; + +const parseSnapshot = (raw: string | undefined): CacheSnapshot => { + if (!raw) { + return {}; + } + + try { + return JSON.parse(raw) as CacheSnapshot; + } catch (error) { + console.warn( + 'Failed to parse the metroSnapshot input. Treating the pre-run cache state as empty.', + error + ); + return {}; + } +}; + +const parseSavePolicyMode = ( + raw: string | undefined +): SavePolicy['mode'] => { + if (raw === 'always' || raw === 'never' || raw === 'default-branch') { + return raw; + } + + return 'default-branch'; +}; + +const run = async (): Promise => { + try { + const projectRootInput = process.env.INPUT_PROJECTROOT; + + const projectRoot = projectRootInput + ? path.resolve(projectRootInput) + : process.cwd(); + + console.info(`Planning Metro cache save for: ${projectRoot}`); + + const { config, projectRoot: resolvedProjectRoot } = await getConfig( + projectRoot + ); + + const githubOutput = process.env.GITHUB_OUTPUT; + if (!githubOutput) { + throw new Error('GITHUB_OUTPUT environment variable is not set'); + } + + // Recomputed rather than threaded through GITHUB_OUTPUT from + // plan-restore: computeMetroStaticInputs is deterministic given the same + // repoRoot/bundlerMetroVersion/salt, so this step can run independently. + const staticInputs = resolveMetroStaticInputs({ + projectRoot: resolvedProjectRoot, + cacheVersionSalt: config.cache?.version, + }); + + const before = parseSnapshot(process.env.INPUT_METRO_SNAPSHOT); + const policy: SavePolicy = { + mode: parseSavePolicyMode(process.env.INPUT_CACHESAVEPOLICY), + isDefaultBranch: process.env.IS_DEFAULT_BRANCH === 'true', + }; + + const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); + const os = process.env.RUNNER_OS ?? process.platform; + + const savePlan = cache.planSave(before, policy, { + metro: { os, staticInputs }, + }); + cache.writeKeysFile(savePlan); + + const metroPlan = savePlan.domains.metro; + + const output = + `metroShouldSave=${metroPlan?.shouldSave ? 'true' : 'false'}\n` + + `metroSaveKey=${metroPlan?.saveKey ?? ''}\n`; + + fs.appendFileSync(githubOutput, output); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } else { + console.error('Failed to plan Metro cache save'); + } + + process.exit(1); + } +}; + +run(); diff --git a/packages/github-action/src/shared/snapshot-metro.ts b/packages/github-action/src/shared/snapshot-metro.ts new file mode 100644 index 00000000..041c48a5 --- /dev/null +++ b/packages/github-action/src/shared/snapshot-metro.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createHarnessCache } from '@react-native-harness/cache'; +import { getConfig } from '@react-native-harness/config'; + +/** + * Must run AFTER the "Restore Metro cache" step and BEFORE the test run, so + * this snapshot reflects exactly what was restored from a prior save (empty + * on a cache miss) -- not the pre-restore, always-empty state of a fresh + * checkout. plan-save.ts diffs its own post-run snapshot against this one, so + * capturing it too early would make every run look like it added the whole + * restored cache, defeating the "only save when content actually changed" + * policy. + */ +const run = async (): Promise => { + try { + const projectRootInput = process.env.INPUT_PROJECTROOT; + + const projectRoot = projectRootInput + ? path.resolve(projectRootInput) + : process.cwd(); + + console.info(`Snapshotting Metro cache for: ${projectRoot}`); + + const { projectRoot: resolvedProjectRoot } = await getConfig(projectRoot); + + const githubOutput = process.env.GITHUB_OUTPUT; + if (!githubOutput) { + throw new Error('GITHUB_OUTPUT environment variable is not set'); + } + + const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); + const snapshotAfterRestore = cache.snapshot(); + + const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`; + + fs.appendFileSync(githubOutput, output); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); + } else { + console.error('Failed to snapshot the Metro cache'); + } + + process.exit(1); + } +}; + +run(); diff --git a/packages/github-action/tsconfig.json b/packages/github-action/tsconfig.json index 399ec7ae..fd0f045e 100644 --- a/packages/github-action/tsconfig.json +++ b/packages/github-action/tsconfig.json @@ -4,11 +4,17 @@ "include": [], "references": [ { - "path": "../config" + "path": "../bundler-metro" }, { "path": "../platform-android" }, + { + "path": "../config" + }, + { + "path": "../cache" + }, { "path": "./tsconfig.lib.json" } diff --git a/packages/github-action/tsconfig.lib.json b/packages/github-action/tsconfig.lib.json index 8459915f..a528a5cc 100644 --- a/packages/github-action/tsconfig.lib.json +++ b/packages/github-action/tsconfig.lib.json @@ -12,10 +12,16 @@ "include": ["src/**/*.ts"], "references": [ { - "path": "../config/tsconfig.lib.json" + "path": "../bundler-metro/tsconfig.lib.json" }, { "path": "../platform-android/tsconfig.lib.json" + }, + { + "path": "../config/tsconfig.lib.json" + }, + { + "path": "../cache/tsconfig.lib.json" } ] } diff --git a/packages/github-action/tsup.config.mts b/packages/github-action/tsup.config.mts index db836be2..47cc3626 100644 --- a/packages/github-action/tsup.config.mts +++ b/packages/github-action/tsup.config.mts @@ -15,6 +15,9 @@ export default defineConfig({ TARGETS.map((target) => [`${target}/index`, `src/${target}/index.ts`]) ), 'shared/index': 'src/shared/index.ts', + 'shared/plan-restore': 'src/shared/plan-restore.ts', + 'shared/snapshot-metro': 'src/shared/snapshot-metro.ts', + 'shared/plan-save': 'src/shared/plan-save.ts', }, outDir: OUT_DIR, format: 'cjs', diff --git a/packages/github-action/vite.config.ts b/packages/github-action/vite.config.ts new file mode 100644 index 00000000..9db8823c --- /dev/null +++ b/packages/github-action/vite.config.ts @@ -0,0 +1,18 @@ +/// +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/github-action', + test: { + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + }, + }, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 239dda3c..f060f58e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -371,6 +371,9 @@ importers: packages/github-action: dependencies: + '@react-native-harness/cache': + specifier: workspace:* + version: link:../cache '@react-native-harness/config': specifier: workspace:* version: link:../config diff --git a/website/src/docs/guides/ci-cd.md b/website/src/docs/guides/ci-cd.md index ae17381f..fa1f769a 100644 --- a/website/src/docs/guides/ci-cd.md +++ b/website/src/docs/guides/ci-cd.md @@ -45,6 +45,7 @@ The action accepts the following inputs: - `cacheAvd` (optional, Android only): Whether to cache the Android Virtual Device snapshot. Defaults to `true`. This is most useful when your Android runner defines AVD details in `rn-harness.config.mjs`. - `preRunHook` (optional): Inline shell script run in `bash` immediately before Harness starts - `afterRunHook` (optional): Inline shell script run in `bash` immediately after Harness finishes and before artifacts are uploaded +- `cacheSavePolicy` (optional): When to save a new Metro cache entry -- `default-branch` (default), `always`, or `never`. See [Metro cache](#metro-cache) below ## Crash Artifacts @@ -249,7 +250,17 @@ If your workflow does not define AVD details, the action can still run the tests React Native Harness persists Metro's transform cache under `.harness/cache/metro` and its file-map (haste) cache under `.harness/cache/metro-file-map`, both in your project root. This is on by default (`cache.metro: true`); set `cache.metro: false` in config to disable it. The deprecated `unstable__enableMetroCache` option is still honored as an alias for one release. -When you use the `callstackincubator/react-native-harness` GitHub Action, Metro cache restoration and saving is handled automatically for the resolved `projectRoot`. You do not need to add a separate `actions/cache` step for `.harness/cache/metro` or `.harness/cache/metro-file-map`. +When you use the `callstackincubator/react-native-harness` GitHub Action, Metro cache restoration and saving is handled automatically for the resolved `projectRoot`. Do not add your own `actions/cache` step for `.harness/cache/metro` or `.harness/cache/metro-file-map` -- the action's cache key scheme assumes it's the only thing writing and reading those entries, and a manually-added step would fight it for ownership of the same paths. + +The cache key is computed by Harness itself, not a static file list: it hashes your lockfile(s) and Metro/Babel config together with the resolved `@react-native-harness/bundler-metro` version and your `cache.version` salt. This means the cache invalidates automatically whenever you upgrade `@react-native-harness/bundler-metro`, not only when a lockfile or config file changes. + +Use the `cacheSavePolicy` input to control when a new cache entry is saved: + +- `default-branch` (default): only save from your default branch, so pull request runs restore from the shared cache but never write to it. This keeps PR runs cache-neutral and avoids cache churn from short-lived branches. +- `always`: save a new entry from every run that changed the cache contents, regardless of branch. Useful if you want branches to build on each other's cache, at the cost of more cache writes and storage churn. +- `never`: never save, only restore. Useful if you manage the Metro cache some other way and only want the action to consume it. + +A new entry is only saved when the run actually changed the cache's contents -- an unchanged, fully cache-hit run does not create a redundant save. ## Web in CI From 594c0f5726a27b361f695f06957646ced0721310 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 12:18:13 +0200 Subject: [PATCH 3/9] fix: rebuild compiled action bundles with isAccretiveDomain fix The previous commit's plan.ts fix wasn't reflected in the compiled actions/shared/*.cjs bundles. --- actions/shared/plan-restore.cjs | 2 +- actions/shared/plan-save.cjs | 2 +- actions/shared/snapshot-metro.cjs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/actions/shared/plan-restore.cjs b/actions/shared/plan-restore.cjs index 523b0c55..dc133ca9 100644 --- a/actions/shared/plan-restore.cjs +++ b/actions/shared/plan-restore.cjs @@ -384,7 +384,7 @@ var isAccretiveDomain = (domain) => { case "metro": return true; default: - return true; + return false; } }; var hashSortedEntries = (entries) => (0, import_node_crypto.createHash)("sha256").update(entries.slice().sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("\n")).digest("hex").slice(0, HASH_LENGTH); diff --git a/actions/shared/plan-save.cjs b/actions/shared/plan-save.cjs index ab7de645..1db8b5e2 100644 --- a/actions/shared/plan-save.cjs +++ b/actions/shared/plan-save.cjs @@ -384,7 +384,7 @@ var isAccretiveDomain = (domain) => { case "metro": return true; default: - return true; + return false; } }; var hashSortedEntries = (entries) => (0, import_node_crypto.createHash)("sha256").update(entries.slice().sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("\n")).digest("hex").slice(0, HASH_LENGTH); diff --git a/actions/shared/snapshot-metro.cjs b/actions/shared/snapshot-metro.cjs index aa462732..0071a8a7 100644 --- a/actions/shared/snapshot-metro.cjs +++ b/actions/shared/snapshot-metro.cjs @@ -384,7 +384,7 @@ var isAccretiveDomain = (domain) => { case "metro": return true; default: - return true; + return false; } }; var hashSortedEntries = (entries) => (0, import_node_crypto.createHash)("sha256").update(entries.slice().sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("\n")).digest("hex").slice(0, HASH_LENGTH); From 374b7e61e1f0cf45f7735d8a0d597134e8080143 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 18:04:25 +0200 Subject: [PATCH 4/9] fix: resolve bundler-metro version through jest when only a nested dependency Metro cache-key resolution always fell back to "unknown" for consumers where @react-native-harness/bundler-metro is only reachable via @react-native-harness/jest's own node_modules (e.g. pnpm workspaces, where transitive workspace deps aren't hoisted to the project root). Fall back to resolving through jest's package location, mirroring how the Harness runtime actually loads bundler-metro. --- actions/shared/plan-restore.cjs | 17 +++++++- actions/shared/plan-save.cjs | 17 +++++++- packages/github-action/eslint.config.mjs | 4 ++ .../__tests__/metro-cache-inputs.test.ts | 41 +++++++++++++++++++ .../src/shared/metro-cache-inputs.ts | 31 ++++++++++++-- packages/github-action/tsconfig.json | 3 ++ packages/github-action/tsconfig.lib.json | 3 ++ 7 files changed, 108 insertions(+), 8 deletions(-) diff --git a/actions/shared/plan-restore.cjs b/actions/shared/plan-restore.cjs index dc133ca9..710fdbcf 100644 --- a/actions/shared/plan-restore.cjs +++ b/actions/shared/plan-restore.cjs @@ -4914,12 +4914,25 @@ var resolveRepoRoot = (projectRoot) => { dir = parent; } }; -var resolveBundlerMetroVersion = (projectRoot) => { +var resolveBundlerMetroPackageJson = (projectRoot) => { try { - const packageJsonPath = require.resolve( + return require.resolve( "@react-native-harness/bundler-metro/package.json", { paths: [projectRoot] } ); + } catch { + const jestPackageJsonPath = require.resolve( + "@react-native-harness/jest/package.json", + { paths: [projectRoot] } + ); + return require.resolve("@react-native-harness/bundler-metro/package.json", { + paths: [import_node_path11.default.dirname(jestPackageJsonPath)] + }); + } +}; +var resolveBundlerMetroVersion = (projectRoot) => { + try { + const packageJsonPath = resolveBundlerMetroPackageJson(projectRoot); const packageJson = JSON.parse(import_node_fs11.default.readFileSync(packageJsonPath, "utf8")); if (typeof packageJson.version !== "string") { throw new Error('"version" field is missing or not a string'); diff --git a/actions/shared/plan-save.cjs b/actions/shared/plan-save.cjs index 1db8b5e2..b9603eae 100644 --- a/actions/shared/plan-save.cjs +++ b/actions/shared/plan-save.cjs @@ -4914,12 +4914,25 @@ var resolveRepoRoot = (projectRoot) => { dir = parent; } }; -var resolveBundlerMetroVersion = (projectRoot) => { +var resolveBundlerMetroPackageJson = (projectRoot) => { try { - const packageJsonPath = require.resolve( + return require.resolve( "@react-native-harness/bundler-metro/package.json", { paths: [projectRoot] } ); + } catch { + const jestPackageJsonPath = require.resolve( + "@react-native-harness/jest/package.json", + { paths: [projectRoot] } + ); + return require.resolve("@react-native-harness/bundler-metro/package.json", { + paths: [import_node_path11.default.dirname(jestPackageJsonPath)] + }); + } +}; +var resolveBundlerMetroVersion = (projectRoot) => { + try { + const packageJsonPath = resolveBundlerMetroPackageJson(projectRoot); const packageJson = JSON.parse(import_node_fs11.default.readFileSync(packageJsonPath, "utf8")); if (typeof packageJson.version !== "string") { throw new Error('"version" field is missing or not a string'); diff --git a/packages/github-action/eslint.config.mjs b/packages/github-action/eslint.config.mjs index 0f029721..fabd4248 100644 --- a/packages/github-action/eslint.config.mjs +++ b/packages/github-action/eslint.config.mjs @@ -19,6 +19,10 @@ export default [ // consuming project's own installed version at runtime, not // imported/bundled by this package. '@react-native-harness/bundler-metro', + // Referenced only as a require.resolve() fallback root to reach + // bundler-metro when it's nested under jest's own node_modules, + // not imported/bundled by this package. + '@react-native-harness/jest', ], }, ], diff --git a/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts b/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts index 1e38f511..4450aa87 100644 --- a/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts +++ b/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts @@ -70,6 +70,47 @@ describe('resolveBundlerMetroVersion', () => { expect(resolveBundlerMetroVersion(projectRoot)).toBe('9.9.9'); }); + it('resolves the version through @react-native-harness/jest when bundler-metro is only a nested dependency', () => { + const jestPackageDir = path.join( + projectRoot, + 'node_modules', + '@react-native-harness', + 'jest' + ); + fs.mkdirSync(jestPackageDir, { recursive: true }); + fs.writeFileSync( + path.join(jestPackageDir, 'package.json'), + JSON.stringify({ + name: '@react-native-harness/jest', + version: '1.0.0', + main: 'index.js', + }) + ); + fs.writeFileSync(path.join(jestPackageDir, 'index.js'), 'module.exports = {};'); + + const nestedBundlerMetroDir = path.join( + jestPackageDir, + 'node_modules', + '@react-native-harness', + 'bundler-metro' + ); + fs.mkdirSync(nestedBundlerMetroDir, { recursive: true }); + fs.writeFileSync( + path.join(nestedBundlerMetroDir, 'package.json'), + JSON.stringify({ + name: '@react-native-harness/bundler-metro', + version: '2.2.2', + main: 'index.js', + }) + ); + fs.writeFileSync( + path.join(nestedBundlerMetroDir, 'index.js'), + 'module.exports = {};' + ); + + expect(resolveBundlerMetroVersion(projectRoot)).toBe('2.2.2'); + }); + it('falls back to "unknown" and warns when the package cannot be resolved', () => { expect(resolveBundlerMetroVersion(projectRoot)).toBe('unknown'); expect(console.warn).toHaveBeenCalled(); diff --git a/packages/github-action/src/shared/metro-cache-inputs.ts b/packages/github-action/src/shared/metro-cache-inputs.ts index 5623a3cd..2e6e43d5 100644 --- a/packages/github-action/src/shared/metro-cache-inputs.ts +++ b/packages/github-action/src/shared/metro-cache-inputs.ts @@ -28,6 +28,32 @@ export const resolveRepoRoot = (projectRoot: string): string => { } }; +/** + * `@react-native-harness/bundler-metro` is rarely a direct dependency of the + * consuming project — it's pulled in transitively through + * `@react-native-harness/jest` (the jest preset every Harness project + * installs directly). Package managers that don't hoist nested/transitive + * workspace deps to the project root (e.g. pnpm) can only resolve it by + * walking through `@react-native-harness/jest`'s own node_modules, so that's + * tried as a fallback resolution root. + */ +const resolveBundlerMetroPackageJson = (projectRoot: string): string => { + try { + return require.resolve( + '@react-native-harness/bundler-metro/package.json', + { paths: [projectRoot] } + ); + } catch { + const jestPackageJsonPath = require.resolve( + '@react-native-harness/jest/package.json', + { paths: [projectRoot] } + ); + return require.resolve('@react-native-harness/bundler-metro/package.json', { + paths: [path.dirname(jestPackageJsonPath)], + }); + } +}; + /** * Resolved from the consuming project's own installed copy of * `@react-native-harness/bundler-metro`, since that's the actual Metro @@ -37,10 +63,7 @@ export const resolveRepoRoot = (projectRoot: string): string => { */ export const resolveBundlerMetroVersion = (projectRoot: string): string => { try { - const packageJsonPath = require.resolve( - '@react-native-harness/bundler-metro/package.json', - { paths: [projectRoot] } - ); + const packageJsonPath = resolveBundlerMetroPackageJson(projectRoot); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (typeof packageJson.version !== 'string') { diff --git a/packages/github-action/tsconfig.json b/packages/github-action/tsconfig.json index fd0f045e..0fe47aad 100644 --- a/packages/github-action/tsconfig.json +++ b/packages/github-action/tsconfig.json @@ -3,6 +3,9 @@ "files": [], "include": [], "references": [ + { + "path": "../jest" + }, { "path": "../bundler-metro" }, diff --git a/packages/github-action/tsconfig.lib.json b/packages/github-action/tsconfig.lib.json index a528a5cc..c497d568 100644 --- a/packages/github-action/tsconfig.lib.json +++ b/packages/github-action/tsconfig.lib.json @@ -11,6 +11,9 @@ }, "include": ["src/**/*.ts"], "references": [ + { + "path": "../jest/tsconfig.lib.json" + }, { "path": "../bundler-metro/tsconfig.lib.json" }, From 623d38c09dbced039c340ade76f7dc805c12c6a8 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 19:04:20 +0200 Subject: [PATCH 5/9] refactor: discover Metro cache key files via upward ancestor walk Replace the downward repo scan (with its node_modules/dist/build skip list) with an upward walk from projectRoot to the repo root, matching how Metro itself resolves config and where monorepo lockfiles actually live. Sibling workspace packages' configs no longer contribute to the key, so unrelated apps in a monorepo can't spuriously invalidate the cache. Also correct the config candidates (add metro.config.{cts,mts,json} and the .config/metro.* convention, replace babel.config.ts with .cts) and log the restore/save outcome plainly in the job log: which key was restored and its size, and whether a new entry was saved, skipped as unchanged, or blocked by cacheSavePolicy. --- action.yml | 4 + actions/shared/plan-restore.cjs | 88 +++++++-- actions/shared/plan-save.cjs | 113 +++++++++-- actions/shared/snapshot-metro.cjs | 50 ++++- .../cache/src/__tests__/domains/metro.test.ts | 186 +++++++++++++++--- packages/cache/src/domains/metro.ts | 155 ++++++++++++--- packages/github-action/src/action.yml | 4 + .../__tests__/metro-cache-inputs.test.ts | 76 +++++++ .../src/shared/__tests__/plan-save.test.ts | 98 +++++++++ .../shared/__tests__/snapshot-metro.test.ts | 35 ++++ .../github-action/src/shared/format-bytes.ts | 6 + .../src/shared/metro-cache-inputs.ts | 10 +- .../github-action/src/shared/plan-save.ts | 41 ++++ .../src/shared/snapshot-metro.ts | 20 ++ 14 files changed, 783 insertions(+), 103 deletions(-) create mode 100644 packages/github-action/src/shared/format-bytes.ts diff --git a/action.yml b/action.yml index 655608b7..0a6fe34b 100644 --- a/action.yml +++ b/action.yml @@ -77,6 +77,7 @@ runs: run: | node ${{ github.action_path }}/actions/shared/plan-restore.cjs - name: Restore Metro cache (.harness/cache/metro) + id: restore-metro uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -96,6 +97,9 @@ runs: shell: bash env: INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + # Empty on a cache miss; snapshot-metro.cjs uses it to report whether + # the Metro cache was actually restored and from which key. + METRO_RESTORED_KEY: ${{ steps.restore-metro.outputs.cache-matched-key }} run: | node ${{ github.action_path }}/actions/shared/snapshot-metro.cjs - name: Restore Harness cache diff --git a/actions/shared/plan-restore.cjs b/actions/shared/plan-restore.cjs index 710fdbcf..a819c8a5 100644 --- a/actions/shared/plan-restore.cjs +++ b/actions/shared/plan-restore.cjs @@ -549,40 +549,79 @@ var import_node_crypto2 = require("crypto"); var import_node_fs8 = __toESM(require("fs"), 1); var import_node_path9 = __toESM(require("path"), 1); var cacheLogger3 = logger.child("cache"); -var SKIPPED_DIRNAMES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]); -var METRO_KEY_FILENAMES = [ +var LOCKFILE_NAMES = [ "bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", - "yarn.lock", - "metro.config.js", - "metro.config.cjs", - "metro.config.mjs", - "metro.config.ts", + "yarn.lock" +]; +var METRO_CONFIG_EXTENSIONS = [ + "js", + "cjs", + "mjs", + "ts", + "cts", + "mts", + "json" +]; +var METRO_CONFIG_NAMES = METRO_CONFIG_EXTENSIONS.map((extension) => `metro.config.${extension}`); +var METRO_CONFIG_DIR_NAMES = METRO_CONFIG_EXTENSIONS.map((extension) => `.config/metro.${extension}`); +var BABEL_CONFIG_NAMES = [ + "babel.config.json", "babel.config.js", "babel.config.cjs", "babel.config.mjs", - "babel.config.ts", - "babel.config.json" + "babel.config.cts" ]; -var METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); -var collectKeyFiles = (repoRoot, directory, matches) => { - for (const dirent of import_node_fs8.default.readdirSync(directory, { withFileTypes: true })) { - if (dirent.isDirectory()) { - if (SKIPPED_DIRNAMES.has(dirent.name)) { +var METRO_KEY_FILENAMES = [ + ...LOCKFILE_NAMES, + ...METRO_CONFIG_NAMES, + ...METRO_CONFIG_DIR_NAMES, + ...BABEL_CONFIG_NAMES +]; +var collectAncestorDirectories = (projectRoot, repoRoot) => { + const resolvedProjectRoot = import_node_path9.default.resolve(projectRoot); + const resolvedRepoRoot = import_node_path9.default.resolve(repoRoot); + if (resolvedProjectRoot === resolvedRepoRoot) { + return [resolvedProjectRoot]; + } + const relative = import_node_path9.default.relative(resolvedRepoRoot, resolvedProjectRoot); + const isProjectRootInsideRepoRoot = relative !== "" && !relative.startsWith(`..${import_node_path9.default.sep}`) && relative !== ".." && !import_node_path9.default.isAbsolute(relative); + if (!isProjectRootInsideRepoRoot) { + return [resolvedProjectRoot]; + } + const directories = []; + let current = resolvedProjectRoot; + for (; ; ) { + directories.push(current); + if (current === resolvedRepoRoot) { + break; + } + const parent = import_node_path9.default.dirname(current); + if (parent === current) { + break; + } + current = parent; + } + return directories; +}; +var collectKeyFiles = (directories, repoRoot) => { + const matches = []; + for (const directory of directories) { + for (const candidate of METRO_KEY_FILENAMES) { + const fullPath = import_node_path9.default.join(directory, candidate); + if (!import_node_fs8.default.existsSync(fullPath) || !import_node_fs8.default.statSync(fullPath).isFile()) { continue; } - collectKeyFiles(repoRoot, import_node_path9.default.join(directory, dirent.name), matches); - } else if (dirent.isFile() && METRO_KEY_FILENAME_SET.has(dirent.name)) { - const fullPath = import_node_path9.default.join(directory, dirent.name); matches.push({ relativePath: import_node_path9.default.relative(repoRoot, fullPath).split(import_node_path9.default.sep).join("/"), content: import_node_fs8.default.readFileSync(fullPath) }); } } + return matches; }; var hashKeyFiles = (matches) => { const hash = (0, import_node_crypto2.createHash)("sha256"); @@ -596,15 +635,23 @@ var hashKeyFiles = (matches) => { }; var computeMetroStaticInputs = (options) => { try { - const matches = []; - collectKeyFiles(options.repoRoot, options.repoRoot, matches); + const resolvedProjectRoot = import_node_path9.default.resolve(options.projectRoot); + const resolvedRepoRoot = import_node_path9.default.resolve(options.repoRoot); + if (!import_node_fs8.default.statSync(resolvedProjectRoot).isDirectory()) { + throw new Error(`projectRoot "${resolvedProjectRoot}" is not a directory`); + } + if (!import_node_fs8.default.statSync(resolvedRepoRoot).isDirectory()) { + throw new Error(`repoRoot "${resolvedRepoRoot}" is not a directory`); + } + const directories = collectAncestorDirectories(resolvedProjectRoot, resolvedRepoRoot); + const matches = collectKeyFiles(directories, resolvedRepoRoot); return { lockfileHash: hashKeyFiles(matches), bundlerMetroVersion: options.bundlerMetroVersion, ...options.salt ? { salt: options.salt } : {} }; } catch (error) { - cacheLogger3.warn(`Failed to compute Metro cache key inputs for "${options.repoRoot}". Falling back to an always-miss key.`, error); + cacheLogger3.warn(`Failed to compute Metro cache key inputs for "${options.projectRoot}". Falling back to an always-miss key.`, error); return { lockfileHash: "unavailable", bundlerMetroVersion: options.bundlerMetroVersion @@ -4947,6 +4994,7 @@ var resolveBundlerMetroVersion = (projectRoot) => { } }; var resolveMetroStaticInputs = (options) => computeMetroStaticInputs({ + projectRoot: options.projectRoot, repoRoot: resolveRepoRoot(options.projectRoot), bundlerMetroVersion: resolveBundlerMetroVersion(options.projectRoot), salt: options.cacheVersionSalt diff --git a/actions/shared/plan-save.cjs b/actions/shared/plan-save.cjs index b9603eae..dfd15ba1 100644 --- a/actions/shared/plan-save.cjs +++ b/actions/shared/plan-save.cjs @@ -549,40 +549,79 @@ var import_node_crypto2 = require("crypto"); var import_node_fs8 = __toESM(require("fs"), 1); var import_node_path9 = __toESM(require("path"), 1); var cacheLogger3 = logger.child("cache"); -var SKIPPED_DIRNAMES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]); -var METRO_KEY_FILENAMES = [ +var LOCKFILE_NAMES = [ "bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", - "yarn.lock", - "metro.config.js", - "metro.config.cjs", - "metro.config.mjs", - "metro.config.ts", + "yarn.lock" +]; +var METRO_CONFIG_EXTENSIONS = [ + "js", + "cjs", + "mjs", + "ts", + "cts", + "mts", + "json" +]; +var METRO_CONFIG_NAMES = METRO_CONFIG_EXTENSIONS.map((extension) => `metro.config.${extension}`); +var METRO_CONFIG_DIR_NAMES = METRO_CONFIG_EXTENSIONS.map((extension) => `.config/metro.${extension}`); +var BABEL_CONFIG_NAMES = [ + "babel.config.json", "babel.config.js", "babel.config.cjs", "babel.config.mjs", - "babel.config.ts", - "babel.config.json" + "babel.config.cts" ]; -var METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); -var collectKeyFiles = (repoRoot, directory, matches) => { - for (const dirent of import_node_fs8.default.readdirSync(directory, { withFileTypes: true })) { - if (dirent.isDirectory()) { - if (SKIPPED_DIRNAMES.has(dirent.name)) { +var METRO_KEY_FILENAMES = [ + ...LOCKFILE_NAMES, + ...METRO_CONFIG_NAMES, + ...METRO_CONFIG_DIR_NAMES, + ...BABEL_CONFIG_NAMES +]; +var collectAncestorDirectories = (projectRoot, repoRoot) => { + const resolvedProjectRoot = import_node_path9.default.resolve(projectRoot); + const resolvedRepoRoot = import_node_path9.default.resolve(repoRoot); + if (resolvedProjectRoot === resolvedRepoRoot) { + return [resolvedProjectRoot]; + } + const relative = import_node_path9.default.relative(resolvedRepoRoot, resolvedProjectRoot); + const isProjectRootInsideRepoRoot = relative !== "" && !relative.startsWith(`..${import_node_path9.default.sep}`) && relative !== ".." && !import_node_path9.default.isAbsolute(relative); + if (!isProjectRootInsideRepoRoot) { + return [resolvedProjectRoot]; + } + const directories = []; + let current = resolvedProjectRoot; + for (; ; ) { + directories.push(current); + if (current === resolvedRepoRoot) { + break; + } + const parent = import_node_path9.default.dirname(current); + if (parent === current) { + break; + } + current = parent; + } + return directories; +}; +var collectKeyFiles = (directories, repoRoot) => { + const matches = []; + for (const directory of directories) { + for (const candidate of METRO_KEY_FILENAMES) { + const fullPath = import_node_path9.default.join(directory, candidate); + if (!import_node_fs8.default.existsSync(fullPath) || !import_node_fs8.default.statSync(fullPath).isFile()) { continue; } - collectKeyFiles(repoRoot, import_node_path9.default.join(directory, dirent.name), matches); - } else if (dirent.isFile() && METRO_KEY_FILENAME_SET.has(dirent.name)) { - const fullPath = import_node_path9.default.join(directory, dirent.name); matches.push({ relativePath: import_node_path9.default.relative(repoRoot, fullPath).split(import_node_path9.default.sep).join("/"), content: import_node_fs8.default.readFileSync(fullPath) }); } } + return matches; }; var hashKeyFiles = (matches) => { const hash = (0, import_node_crypto2.createHash)("sha256"); @@ -596,15 +635,23 @@ var hashKeyFiles = (matches) => { }; var computeMetroStaticInputs = (options) => { try { - const matches = []; - collectKeyFiles(options.repoRoot, options.repoRoot, matches); + const resolvedProjectRoot = import_node_path9.default.resolve(options.projectRoot); + const resolvedRepoRoot = import_node_path9.default.resolve(options.repoRoot); + if (!import_node_fs8.default.statSync(resolvedProjectRoot).isDirectory()) { + throw new Error(`projectRoot "${resolvedProjectRoot}" is not a directory`); + } + if (!import_node_fs8.default.statSync(resolvedRepoRoot).isDirectory()) { + throw new Error(`repoRoot "${resolvedRepoRoot}" is not a directory`); + } + const directories = collectAncestorDirectories(resolvedProjectRoot, resolvedRepoRoot); + const matches = collectKeyFiles(directories, resolvedRepoRoot); return { lockfileHash: hashKeyFiles(matches), bundlerMetroVersion: options.bundlerMetroVersion, ...options.salt ? { salt: options.salt } : {} }; } catch (error) { - cacheLogger3.warn(`Failed to compute Metro cache key inputs for "${options.repoRoot}". Falling back to an always-miss key.`, error); + cacheLogger3.warn(`Failed to compute Metro cache key inputs for "${options.projectRoot}". Falling back to an always-miss key.`, error); return { lockfileHash: "unavailable", bundlerMetroVersion: options.bundlerMetroVersion @@ -4897,6 +4944,9 @@ var getConfig = async (dir) => { }; }; +// src/shared/format-bytes.ts +var formatMegabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + // src/shared/metro-cache-inputs.ts var import_node_fs11 = __toESM(require("fs")); var import_node_path11 = __toESM(require("path")); @@ -4947,6 +4997,7 @@ var resolveBundlerMetroVersion = (projectRoot) => { } }; var resolveMetroStaticInputs = (options) => computeMetroStaticInputs({ + projectRoot: options.projectRoot, repoRoot: resolveRepoRoot(options.projectRoot), bundlerMetroVersion: resolveBundlerMetroVersion(options.projectRoot), salt: options.cacheVersionSalt @@ -4973,6 +5024,27 @@ var parseSavePolicyMode = (raw) => { } return "default-branch"; }; +var logMetroSaveDecision = (plan, policy) => { + if (!plan) { + return; + } + const { delta, shouldSave, saveKey } = plan; + if (delta.addedEntries + delta.removedEntries === 0) { + console.info("Metro cache: unchanged \u2014 skipping save."); + return; + } + const change = `+${delta.addedEntries} files / ${formatMegabytes(delta.addedBytes)} added, ${delta.removedEntries} removed`; + if (shouldSave) { + console.info( + `Metro cache: changed (${change}) \u2014 saving new entry with key "${saveKey}".` + ); + return; + } + const reason = policy.mode === "default-branch" ? 'policy "default-branch" (current branch is not the default branch)' : `policy "${policy.mode}"`; + console.info( + `Metro cache: changed (${change}) but save skipped by ${reason}.` + ); +}; var run = async () => { try { const projectRootInput = process.env.INPUT_PROJECTROOT; @@ -5001,6 +5073,7 @@ var run = async () => { }); cache.writeKeysFile(savePlan); const metroPlan = savePlan.domains.metro; + logMetroSaveDecision(metroPlan, policy); const output = `metroShouldSave=${metroPlan?.shouldSave ? "true" : "false"} metroSaveKey=${metroPlan?.saveKey ?? ""} `; diff --git a/actions/shared/snapshot-metro.cjs b/actions/shared/snapshot-metro.cjs index 0071a8a7..74e94c5e 100644 --- a/actions/shared/snapshot-metro.cjs +++ b/actions/shared/snapshot-metro.cjs @@ -549,24 +549,38 @@ var import_node_crypto2 = require("crypto"); var import_node_fs8 = __toESM(require("fs"), 1); var import_node_path9 = __toESM(require("path"), 1); var cacheLogger3 = logger.child("cache"); -var METRO_KEY_FILENAMES = [ +var LOCKFILE_NAMES = [ "bun.lock", "bun.lockb", "package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", - "yarn.lock", - "metro.config.js", - "metro.config.cjs", - "metro.config.mjs", - "metro.config.ts", + "yarn.lock" +]; +var METRO_CONFIG_EXTENSIONS = [ + "js", + "cjs", + "mjs", + "ts", + "cts", + "mts", + "json" +]; +var METRO_CONFIG_NAMES = METRO_CONFIG_EXTENSIONS.map((extension) => `metro.config.${extension}`); +var METRO_CONFIG_DIR_NAMES = METRO_CONFIG_EXTENSIONS.map((extension) => `.config/metro.${extension}`); +var BABEL_CONFIG_NAMES = [ + "babel.config.json", "babel.config.js", "babel.config.cjs", "babel.config.mjs", - "babel.config.ts", - "babel.config.json" + "babel.config.cts" +]; +var METRO_KEY_FILENAMES = [ + ...LOCKFILE_NAMES, + ...METRO_CONFIG_NAMES, + ...METRO_CONFIG_DIR_NAMES, + ...BABEL_CONFIG_NAMES ]; -var METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); // ../cache/dist/index.js var cacheLogger4 = logger.child("cache"); @@ -4853,6 +4867,9 @@ var getConfig = async (dir) => { }; }; +// src/shared/format-bytes.ts +var formatMegabytes = (bytes) => `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + // src/shared/snapshot-metro.ts var run = async () => { try { @@ -4866,6 +4883,21 @@ var run = async () => { } const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); const snapshotAfterRestore = cache.snapshot(); + const restoredKey = process.env.METRO_RESTORED_KEY; + if (restoredKey) { + const metroEntries = snapshotAfterRestore.metro ?? []; + const totalBytes = metroEntries.reduce( + (sum, entry) => sum + entry.sizeBytes, + 0 + ); + console.info( + `Metro cache: restored from key "${restoredKey}" (${metroEntries.length} files, ${formatMegabytes(totalBytes)}).` + ); + } else { + console.info( + "Metro cache: no existing cache entry matched \u2014 starting cold." + ); + } const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)} `; import_node_fs11.default.appendFileSync(githubOutput, output); diff --git a/packages/cache/src/__tests__/domains/metro.test.ts b/packages/cache/src/__tests__/domains/metro.test.ts index 58f3aad8..96f3f308 100644 --- a/packages/cache/src/__tests__/domains/metro.test.ts +++ b/packages/cache/src/__tests__/domains/metro.test.ts @@ -6,45 +6,67 @@ import { computeMetroStaticInputs } from '../../domains/metro.js'; describe('computeMetroStaticInputs', () => { let repoRoot: string; + let projectRoot: string; beforeEach(() => { repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-cache-metro-')); + projectRoot = path.join(repoRoot, 'apps', 'mobile'); + fs.mkdirSync(projectRoot, { recursive: true }); }); afterEach(() => { fs.rmSync(repoRoot, { recursive: true, force: true }); }); - it('only hashes recognized key filenames, ignoring unrelated files', () => { + it('includes a lockfile at an ancestor directory between projectRoot and repoRoot', () => { fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); - fs.mkdirSync(path.join(repoRoot, 'nested'), { recursive: true }); - fs.writeFileSync( - path.join(repoRoot, 'nested', 'metro.config.js'), - 'config-a' - ); - fs.writeFileSync(path.join(repoRoot, 'README.md'), 'irrelevant'); - const withoutReadme = computeMetroStaticInputs({ + const before = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-b'); + + const after = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(before.lockfileHash).not.toBe(after.lockfileHash); + }); + + it('ignores a config file in a sibling directory not on the ancestor chain', () => { + const siblingDir = path.join(repoRoot, 'apps', 'other'); + fs.mkdirSync(siblingDir, { recursive: true }); + fs.writeFileSync(path.join(siblingDir, 'metro.config.js'), 'config-a'); + + const before = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); - fs.writeFileSync(path.join(repoRoot, 'README.md'), 'changed irrelevant'); + fs.writeFileSync(path.join(siblingDir, 'metro.config.js'), 'config-b'); - const withChangedReadme = computeMetroStaticInputs({ + const after = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); - expect(withoutReadme.lockfileHash).toBe(withChangedReadme.lockfileHash); + expect(before.lockfileHash).toBe(after.lockfileHash); }); - it('excludes matched files vendored under node_modules', () => { - // This is the correctness fix over today's - // `hashFiles('**/pnpm-lock.yaml', ...)`, which globs inside - // node_modules and would pick up a dependency's own lockfile. + it('excludes a vendored lockfile under node_modules (regression pin)', () => { + // Pin for the old bug where a downward `hashFiles('**/pnpm-lock.yaml', + // ...)`-style glob would match a dependency's own lockfile under + // node_modules. Trivially true now that node_modules is never on the + // upward ancestor chain, but kept as an explicit regression test. fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); - const vendoredDir = path.join(repoRoot, 'node_modules', 'some-pkg'); + const vendoredDir = path.join(projectRoot, 'node_modules', 'some-pkg'); fs.mkdirSync(vendoredDir, { recursive: true }); fs.writeFileSync( path.join(vendoredDir, 'package-lock.json'), @@ -52,16 +74,18 @@ describe('computeMetroStaticInputs', () => { ); const withVendored = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); - fs.rmSync(path.join(repoRoot, 'node_modules'), { + fs.rmSync(path.join(projectRoot, 'node_modules'), { recursive: true, force: true, }); const withoutVendored = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); @@ -71,13 +95,15 @@ describe('computeMetroStaticInputs', () => { it('is deterministic across independent calls given the same fixture tree', () => { fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); - fs.writeFileSync(path.join(repoRoot, 'metro.config.js'), 'config-a'); + fs.writeFileSync(path.join(projectRoot, 'metro.config.js'), 'config-a'); const first = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); const second = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); @@ -89,6 +115,7 @@ describe('computeMetroStaticInputs', () => { fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); const before = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); @@ -96,6 +123,7 @@ describe('computeMetroStaticInputs', () => { fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-b'); const after = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); @@ -105,16 +133,18 @@ describe('computeMetroStaticInputs', () => { it('does not change the hash when an unrelated file changes content', () => { fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); - fs.writeFileSync(path.join(repoRoot, 'notes.txt'), 'v1'); + fs.writeFileSync(path.join(projectRoot, 'notes.txt'), 'v1'); const before = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); - fs.writeFileSync(path.join(repoRoot, 'notes.txt'), 'v2'); + fs.writeFileSync(path.join(projectRoot, 'notes.txt'), 'v2'); const after = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '1.0.0', }); @@ -122,10 +152,85 @@ describe('computeMetroStaticInputs', () => { expect(before.lockfileHash).toBe(after.lockfileHash); }); + it.each([ + ['metro.config.mts', 'metro-mts-a', 'metro-mts-b'], + ['metro.config.cts', 'metro-cts-a', 'metro-cts-b'], + ['metro.config.json', 'metro-json-a', 'metro-json-b'], + ['babel.config.cts', 'babel-cts-a', 'babel-cts-b'], + ])('picks up %s as a candidate at projectRoot', (filename, before, after) => { + fs.writeFileSync(path.join(projectRoot, filename), before); + + const beforeInputs = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(projectRoot, filename), after); + + const afterInputs = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(beforeInputs.lockfileHash).not.toBe(afterInputs.lockfileHash); + }); + + it('picks up .config/metro.js as a candidate at projectRoot', () => { + const configDir = path.join(projectRoot, '.config'); + fs.mkdirSync(configDir, { recursive: true }); + fs.writeFileSync(path.join(configDir, 'metro.js'), 'config-a'); + + const before = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(configDir, 'metro.js'), 'config-b'); + + const after = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(before.lockfileHash).not.toBe(after.lockfileHash); + }); + + it('no longer treats babel.config.ts as a candidate', () => { + const before = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(projectRoot, 'babel.config.ts'), 'babel-ts-a'); + + const afterCreate = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(projectRoot, 'babel.config.ts'), 'babel-ts-b'); + + const afterChange = computeMetroStaticInputs({ + projectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(afterCreate.lockfileHash).toBe(before.lockfileHash); + expect(afterChange.lockfileHash).toBe(before.lockfileHash); + }); + it('passes bundlerMetroVersion and salt through into the returned object', () => { fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); const withoutSalt = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '2.3.4', }); @@ -133,6 +238,7 @@ describe('computeMetroStaticInputs', () => { expect(withoutSalt.salt).toBeUndefined(); const withSalt = computeMetroStaticInputs({ + projectRoot, repoRoot, bundlerMetroVersion: '2.3.4', salt: 'v2', @@ -140,11 +246,27 @@ describe('computeMetroStaticInputs', () => { expect(withSalt.salt).toBe('v2'); }); + it('returns a stable fallback instead of throwing when projectRoot does not exist', () => { + const missingProjectRoot = path.join(repoRoot, 'does-not-exist'); + + const result = computeMetroStaticInputs({ + projectRoot: missingProjectRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(result).toEqual({ + lockfileHash: 'unavailable', + bundlerMetroVersion: '1.0.0', + }); + }); + it('returns a stable fallback instead of throwing when repoRoot does not exist', () => { - const missingRoot = path.join(repoRoot, 'does-not-exist'); + const missingRepoRoot = path.join(repoRoot, 'does-not-exist'); const result = computeMetroStaticInputs({ - repoRoot: missingRoot, + projectRoot, + repoRoot: missingRepoRoot, bundlerMetroVersion: '1.0.0', }); @@ -153,4 +275,24 @@ describe('computeMetroStaticInputs', () => { bundlerMetroVersion: '1.0.0', }); }); + + it('works when projectRoot === repoRoot (single directory examined)', () => { + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-a'); + + const before = computeMetroStaticInputs({ + projectRoot: repoRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + fs.writeFileSync(path.join(repoRoot, 'pnpm-lock.yaml'), 'lockfile-b'); + + const after = computeMetroStaticInputs({ + projectRoot: repoRoot, + repoRoot, + bundlerMetroVersion: '1.0.0', + }); + + expect(before.lockfileHash).not.toBe(after.lockfileHash); + }); }); diff --git a/packages/cache/src/domains/metro.ts b/packages/cache/src/domains/metro.ts index e361873f..02dcb8ac 100644 --- a/packages/cache/src/domains/metro.ts +++ b/packages/cache/src/domains/metro.ts @@ -5,51 +5,130 @@ import { logger } from '@react-native-harness/tools'; const cacheLogger = logger.child('cache'); -// Directories that never contain a repo's own lockfiles/config, only vendored -// or generated copies of them (e.g. a dependency's own package-lock.json -// under node_modules). Skipping these is the correctness fix over today's -// `hashFiles('**/pnpm-lock.yaml', ...)` glob, which matches inside -// node_modules too. -const SKIPPED_DIRNAMES = new Set(['node_modules', '.git', 'dist', 'build']); - -export const METRO_KEY_FILENAMES: readonly string[] = [ +// Metro resolves its own config by walking UP from the project root (see +// Metro's loadConfig docs), never by scanning the tree downward. Lockfiles in +// a monorepo live at the repo root, i.e. somewhere on projectRoot's ancestor +// chain. So instead of a downward `hashFiles('**/…')`-style walk (which also +// has to dodge node_modules/.git/dist/build to avoid vendored copies), we +// walk upward from projectRoot to repoRoot (inclusive) and hash every +// candidate key file that exists along that chain. Sibling workspace +// packages are never on this chain, so their configs can't spuriously +// invalidate this project's cache key. +const LOCKFILE_NAMES: readonly string[] = [ 'bun.lock', 'bun.lockb', 'package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', - 'metro.config.js', - 'metro.config.cjs', - 'metro.config.mjs', - 'metro.config.ts', +]; + +// Metro config candidates: `metro.config.*` at the directory root, plus the +// `.config/metro.*` directory convention Metro also resolves. +const METRO_CONFIG_EXTENSIONS: readonly string[] = [ + 'js', + 'cjs', + 'mjs', + 'ts', + 'cts', + 'mts', + 'json', +]; +const METRO_CONFIG_NAMES: readonly string[] = METRO_CONFIG_EXTENSIONS.map( + (extension) => `metro.config.${extension}` +); +const METRO_CONFIG_DIR_NAMES: readonly string[] = METRO_CONFIG_EXTENSIONS.map( + (extension) => `.config/metro.${extension}` +); + +// Babel only resolves root configs named `babel.config.*` (note: no `.ts` — +// Babel does not resolve `babel.config.ts`). File-relative `.babelrc` and the +// `package.json` `babel`/`metro` fields are intentionally out of scope. +const BABEL_CONFIG_NAMES: readonly string[] = [ + 'babel.config.json', 'babel.config.js', 'babel.config.cjs', 'babel.config.mjs', - 'babel.config.ts', - 'babel.config.json', + 'babel.config.cts', ]; -const METRO_KEY_FILENAME_SET = new Set(METRO_KEY_FILENAMES); +// Relative-to-examined-directory paths checked at every directory on the +// ancestor chain. All candidates that exist are hashed (rather than +// emulating Metro/Babel's own first-match-wins resolution), which is +// deterministic and strictly more conservative. +export const METRO_KEY_FILENAMES: readonly string[] = [ + ...LOCKFILE_NAMES, + ...METRO_CONFIG_NAMES, + ...METRO_CONFIG_DIR_NAMES, + ...BABEL_CONFIG_NAMES, +]; interface KeyFileMatch { relativePath: string; content: Buffer; } +/** + * Directories to examine: projectRoot and every ancestor directory up to and + * including repoRoot. Defensive: if projectRoot isn't inside repoRoot (or + * repoRoot can't be related to it), fall back to examining projectRoot alone + * rather than guessing. + */ +const collectAncestorDirectories = ( + projectRoot: string, + repoRoot: string +): string[] => { + const resolvedProjectRoot = path.resolve(projectRoot); + const resolvedRepoRoot = path.resolve(repoRoot); + + if (resolvedProjectRoot === resolvedRepoRoot) { + return [resolvedProjectRoot]; + } + + const relative = path.relative(resolvedRepoRoot, resolvedProjectRoot); + const isProjectRootInsideRepoRoot = + relative !== '' && + !relative.startsWith(`..${path.sep}`) && + relative !== '..' && + !path.isAbsolute(relative); + + if (!isProjectRootInsideRepoRoot) { + return [resolvedProjectRoot]; + } + + const directories: string[] = []; + let current = resolvedProjectRoot; + for (;;) { + directories.push(current); + if (current === resolvedRepoRoot) { + break; + } + const parent = path.dirname(current); + if (parent === current) { + // Reached the filesystem root without hitting repoRoot; defensive + // guard against an infinite loop. + break; + } + current = parent; + } + + return directories; +}; + const collectKeyFiles = ( - repoRoot: string, - directory: string, - matches: KeyFileMatch[] -): void => { - for (const dirent of fs.readdirSync(directory, { withFileTypes: true })) { - if (dirent.isDirectory()) { - if (SKIPPED_DIRNAMES.has(dirent.name)) { + directories: readonly string[], + repoRoot: string +): KeyFileMatch[] => { + const matches: KeyFileMatch[] = []; + + for (const directory of directories) { + for (const candidate of METRO_KEY_FILENAMES) { + const fullPath = path.join(directory, candidate); + + if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) { continue; } - collectKeyFiles(repoRoot, path.join(directory, dirent.name), matches); - } else if (dirent.isFile() && METRO_KEY_FILENAME_SET.has(dirent.name)) { - const fullPath = path.join(directory, dirent.name); + matches.push({ relativePath: path .relative(repoRoot, fullPath) @@ -59,6 +138,8 @@ const collectKeyFiles = ( }); } } + + return matches; }; const hashKeyFiles = (matches: KeyFileMatch[]): string => { @@ -77,13 +158,31 @@ const hashKeyFiles = (matches: KeyFileMatch[]): string => { }; export const computeMetroStaticInputs = (options: { + projectRoot: string; repoRoot: string; bundlerMetroVersion: string; salt?: string; }): Record => { try { - const matches: KeyFileMatch[] = []; - collectKeyFiles(options.repoRoot, options.repoRoot, matches); + const resolvedProjectRoot = path.resolve(options.projectRoot); + const resolvedRepoRoot = path.resolve(options.repoRoot); + + // Validate both roots exist so a missing directory degrades to the + // always-miss fallback below instead of silently hashing zero files. + if (!fs.statSync(resolvedProjectRoot).isDirectory()) { + throw new Error( + `projectRoot "${resolvedProjectRoot}" is not a directory` + ); + } + if (!fs.statSync(resolvedRepoRoot).isDirectory()) { + throw new Error(`repoRoot "${resolvedRepoRoot}" is not a directory`); + } + + const directories = collectAncestorDirectories( + resolvedProjectRoot, + resolvedRepoRoot + ); + const matches = collectKeyFiles(directories, resolvedRepoRoot); return { lockfileHash: hashKeyFiles(matches), @@ -92,7 +191,7 @@ export const computeMetroStaticInputs = (options: { }; } catch (error) { cacheLogger.warn( - `Failed to compute Metro cache key inputs for "${options.repoRoot}". Falling back to an always-miss key.`, + `Failed to compute Metro cache key inputs for "${options.projectRoot}". Falling back to an always-miss key.`, error ); return { diff --git a/packages/github-action/src/action.yml b/packages/github-action/src/action.yml index 655608b7..0a6fe34b 100644 --- a/packages/github-action/src/action.yml +++ b/packages/github-action/src/action.yml @@ -77,6 +77,7 @@ runs: run: | node ${{ github.action_path }}/actions/shared/plan-restore.cjs - name: Restore Metro cache (.harness/cache/metro) + id: restore-metro uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -96,6 +97,9 @@ runs: shell: bash env: INPUT_PROJECTROOT: ${{ steps.load-config.outputs.projectRoot }} + # Empty on a cache miss; snapshot-metro.cjs uses it to report whether + # the Metro cache was actually restored and from which key. + METRO_RESTORED_KEY: ${{ steps.restore-metro.outputs.cache-matched-key }} run: | node ${{ github.action_path }}/actions/shared/snapshot-metro.cjs - name: Restore Harness cache diff --git a/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts b/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts index 4450aa87..5838a36d 100644 --- a/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts +++ b/packages/github-action/src/shared/__tests__/metro-cache-inputs.test.ts @@ -4,9 +4,20 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { resolveBundlerMetroVersion, + resolveMetroStaticInputs, resolveRepoRoot, } from '../metro-cache-inputs.js'; +const mocks = vi.hoisted(() => ({ + computeMetroStaticInputs: vi.fn(), +})); + +// This package's job is wiring, not re-testing @react-native-harness/cache's +// own logic, so its exports are mocked rather than exercised for real. +vi.mock('@react-native-harness/cache', () => ({ + computeMetroStaticInputs: mocks.computeMetroStaticInputs, +})); + describe('resolveRepoRoot', () => { let root: string; @@ -116,3 +127,68 @@ describe('resolveBundlerMetroVersion', () => { expect(console.warn).toHaveBeenCalled(); }); }); + +describe('resolveMetroStaticInputs', () => { + let repoRoot: string; + let projectRoot: string; + + beforeEach(() => { + vi.clearAllMocks(); + + repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-static-inputs-')); + fs.mkdirSync(path.join(repoRoot, '.git')); + projectRoot = path.join(repoRoot, 'apps', 'mobile'); + + const packageDir = path.join( + projectRoot, + 'node_modules', + '@react-native-harness', + 'bundler-metro' + ); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync( + path.join(packageDir, 'package.json'), + JSON.stringify({ + name: '@react-native-harness/bundler-metro', + version: '9.9.9', + main: 'index.js', + }) + ); + fs.writeFileSync(path.join(packageDir, 'index.js'), 'module.exports = {};'); + }); + + afterEach(() => { + fs.rmSync(repoRoot, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + it('forwards projectRoot, the resolved repoRoot, bundlerMetroVersion, and salt', () => { + mocks.computeMetroStaticInputs.mockReturnValue({ lockfileHash: 'abc' }); + + const result = resolveMetroStaticInputs({ + projectRoot, + cacheVersionSalt: 'v2', + }); + + expect(mocks.computeMetroStaticInputs).toHaveBeenCalledWith({ + projectRoot, + repoRoot, + bundlerMetroVersion: '9.9.9', + salt: 'v2', + }); + expect(result).toEqual({ lockfileHash: 'abc' }); + }); + + it('omits the salt when no cacheVersionSalt is provided', () => { + mocks.computeMetroStaticInputs.mockReturnValue({ lockfileHash: 'abc' }); + + resolveMetroStaticInputs({ projectRoot }); + + expect(mocks.computeMetroStaticInputs).toHaveBeenCalledWith({ + projectRoot, + repoRoot, + bundlerMetroVersion: '9.9.9', + salt: undefined, + }); + }); +}); diff --git a/packages/github-action/src/shared/__tests__/plan-save.test.ts b/packages/github-action/src/shared/__tests__/plan-save.test.ts index 839d6438..70dc4910 100644 --- a/packages/github-action/src/shared/__tests__/plan-save.test.ts +++ b/packages/github-action/src/shared/__tests__/plan-save.test.ts @@ -140,6 +140,104 @@ describe('plan-save', () => { ); }); + it('reports the delta and save key when the cache changed and will be saved', async () => { + planSaveMock.mockReturnValue({ + version: 1, + domains: { + metro: { + shouldSave: true, + saveKey: 'harness-metro-linux-abc-contenthash', + delta: { + addedEntries: 3, + removedEntries: 1, + addedBytes: 2 * 1024 * 1024, + }, + }, + }, + }); + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(writeKeysFileMock).toHaveBeenCalled(); + }); + + expect(console.info).toHaveBeenCalledWith( + 'Metro cache: changed (+3 files / 2.0 MB added, 1 removed) — saving new entry with key "harness-metro-linux-abc-contenthash".' + ); + }); + + it('reports an unchanged cache as the reason for skipping the save', async () => { + planSaveMock.mockReturnValue({ + version: 1, + domains: { + metro: { + shouldSave: false, + saveKey: 'harness-metro-linux-abc-contenthash', + delta: { addedEntries: 0, removedEntries: 0, addedBytes: 0 }, + }, + }, + }); + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(writeKeysFileMock).toHaveBeenCalled(); + }); + + expect(console.info).toHaveBeenCalledWith( + 'Metro cache: unchanged — skipping save.' + ); + }); + + it('reports the "default-branch" policy as the reason when a change is not saved off the default branch', async () => { + process.env.IS_DEFAULT_BRANCH = 'false'; + planSaveMock.mockReturnValue({ + version: 1, + domains: { + metro: { + shouldSave: false, + saveKey: 'harness-metro-linux-abc-contenthash', + delta: { addedEntries: 2, removedEntries: 0, addedBytes: 1024 }, + }, + }, + }); + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(writeKeysFileMock).toHaveBeenCalled(); + }); + + expect(console.info).toHaveBeenCalledWith( + 'Metro cache: changed (+2 files / 0.0 MB added, 0 removed) but save skipped by policy "default-branch" (current branch is not the default branch).' + ); + }); + + it('reports the "never" policy as the reason when a change is not saved', async () => { + process.env.INPUT_CACHESAVEPOLICY = 'never'; + planSaveMock.mockReturnValue({ + version: 1, + domains: { + metro: { + shouldSave: false, + saveKey: 'harness-metro-linux-abc-contenthash', + delta: { addedEntries: 2, removedEntries: 0, addedBytes: 1024 }, + }, + }, + }); + + await import('../plan-save.js'); + + await vi.waitFor(() => { + expect(writeKeysFileMock).toHaveBeenCalled(); + }); + + expect(console.info).toHaveBeenCalledWith( + 'Metro cache: changed (+2 files / 0.0 MB added, 0 removed) but save skipped by policy "never".' + ); + }); + it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { delete process.env.GITHUB_OUTPUT; diff --git a/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts b/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts index adf4f09e..69b9b11f 100644 --- a/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts +++ b/packages/github-action/src/shared/__tests__/snapshot-metro.test.ts @@ -54,6 +54,7 @@ describe('snapshot-metro', () => { fs.rmSync(projectRoot, { recursive: true, force: true }); delete process.env.INPUT_PROJECTROOT; delete process.env.GITHUB_OUTPUT; + delete process.env.METRO_RESTORED_KEY; vi.restoreAllMocks(); }); @@ -70,6 +71,40 @@ describe('snapshot-metro', () => { ); }); + it('reports the restored key with file count and size when a cache entry matched', async () => { + process.env.METRO_RESTORED_KEY = 'harness-metro-linux-abc-contenthash'; + snapshotMock.mockReturnValue({ + metro: [ + { path: 'metro/a', sizeBytes: 1024 * 1024, mtimeMs: 1 }, + { path: 'metro/b', sizeBytes: 512 * 1024, mtimeMs: 1 }, + ], + }); + + await import('../snapshot-metro.js'); + + await vi.waitFor(() => { + expect(snapshotMock).toHaveBeenCalled(); + }); + + expect(console.info).toHaveBeenCalledWith( + 'Metro cache: restored from key "harness-metro-linux-abc-contenthash" (2 files, 1.5 MB).' + ); + }); + + it('reports a cold start when no cache entry matched', async () => { + delete process.env.METRO_RESTORED_KEY; + + await import('../snapshot-metro.js'); + + await vi.waitFor(() => { + expect(snapshotMock).toHaveBeenCalled(); + }); + + expect(console.info).toHaveBeenCalledWith( + 'Metro cache: no existing cache entry matched — starting cold.' + ); + }); + it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { delete process.env.GITHUB_OUTPUT; diff --git a/packages/github-action/src/shared/format-bytes.ts b/packages/github-action/src/shared/format-bytes.ts new file mode 100644 index 00000000..143de54b --- /dev/null +++ b/packages/github-action/src/shared/format-bytes.ts @@ -0,0 +1,6 @@ +/** + * Human-readable size for single-line step-log messages, e.g. "12.3 MB". + * Always megabytes so the unit is stable and the lines stay grep-friendly. + */ +export const formatMegabytes = (bytes: number): string => + `${(bytes / (1024 * 1024)).toFixed(1)} MB`; diff --git a/packages/github-action/src/shared/metro-cache-inputs.ts b/packages/github-action/src/shared/metro-cache-inputs.ts index 2e6e43d5..e03f04f4 100644 --- a/packages/github-action/src/shared/metro-cache-inputs.ts +++ b/packages/github-action/src/shared/metro-cache-inputs.ts @@ -6,10 +6,11 @@ const FALLBACK_BUNDLER_METRO_VERSION = 'unknown'; /** * Lockfiles in a monorepo often live above the Harness projectRoot, so the - * Metro key recipe walk anchors at the git repository root when discoverable - * (mirroring how action.yml's `hashFiles('**\/pnpm-lock.yaml', ...)` today - * searches from the workflow's checkout root, not from projectRoot), falling - * back to projectRoot itself for shallow/sparse checkouts with no `.git`. + * Metro key recipe walks the ancestor chain upward from projectRoot. The git + * repository root is the upper boundary of that walk when discoverable, + * ensuring monorepo-root lockfiles above projectRoot are included. Falls + * back to projectRoot itself for shallow/sparse checkouts with no `.git` + * (limiting the walk to projectRoot alone). */ export const resolveRepoRoot = (projectRoot: string): string => { let dir = projectRoot; @@ -92,6 +93,7 @@ export const resolveMetroStaticInputs = (options: { cacheVersionSalt?: string; }): Record => computeMetroStaticInputs({ + projectRoot: options.projectRoot, repoRoot: resolveRepoRoot(options.projectRoot), bundlerMetroVersion: resolveBundlerMetroVersion(options.projectRoot), salt: options.cacheVersionSalt, diff --git a/packages/github-action/src/shared/plan-save.ts b/packages/github-action/src/shared/plan-save.ts index 3fa4e01b..8c071987 100644 --- a/packages/github-action/src/shared/plan-save.ts +++ b/packages/github-action/src/shared/plan-save.ts @@ -3,9 +3,11 @@ import path from 'node:path'; import { createHarnessCache, type CacheSnapshot, + type DomainSavePlan, type SavePolicy, } from '@react-native-harness/cache'; import { getConfig } from '@react-native-harness/config'; +import { formatMegabytes } from './format-bytes.js'; import { resolveMetroStaticInputs } from './metro-cache-inputs.js'; const parseSnapshot = (raw: string | undefined): CacheSnapshot => { @@ -34,6 +36,44 @@ const parseSavePolicyMode = ( return 'default-branch'; }; +/** + * Single-line, grep-friendly job-log report of the save decision and its + * reason, so a skipped "Save Metro cache" step is never a mystery: it + * distinguishes "nothing changed" from "changed but blocked by policy". + */ +const logMetroSaveDecision = ( + plan: DomainSavePlan | undefined, + policy: SavePolicy +): void => { + if (!plan) { + return; + } + + const { delta, shouldSave, saveKey } = plan; + + if (delta.addedEntries + delta.removedEntries === 0) { + console.info('Metro cache: unchanged — skipping save.'); + return; + } + + const change = `+${delta.addedEntries} files / ${formatMegabytes(delta.addedBytes)} added, ${delta.removedEntries} removed`; + + if (shouldSave) { + console.info( + `Metro cache: changed (${change}) — saving new entry with key "${saveKey}".` + ); + return; + } + + const reason = + policy.mode === 'default-branch' + ? 'policy "default-branch" (current branch is not the default branch)' + : `policy "${policy.mode}"`; + console.info( + `Metro cache: changed (${change}) but save skipped by ${reason}.` + ); +}; + const run = async (): Promise => { try { const projectRootInput = process.env.INPUT_PROJECTROOT; @@ -76,6 +116,7 @@ const run = async (): Promise => { cache.writeKeysFile(savePlan); const metroPlan = savePlan.domains.metro; + logMetroSaveDecision(metroPlan, policy); const output = `metroShouldSave=${metroPlan?.shouldSave ? 'true' : 'false'}\n` + diff --git a/packages/github-action/src/shared/snapshot-metro.ts b/packages/github-action/src/shared/snapshot-metro.ts index 041c48a5..281ad55c 100644 --- a/packages/github-action/src/shared/snapshot-metro.ts +++ b/packages/github-action/src/shared/snapshot-metro.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { createHarnessCache } from '@react-native-harness/cache'; import { getConfig } from '@react-native-harness/config'; +import { formatMegabytes } from './format-bytes.js'; /** * Must run AFTER the "Restore Metro cache" step and BEFORE the test run, so @@ -32,6 +33,25 @@ const run = async (): Promise => { const cache = createHarnessCache({ projectRoot: resolvedProjectRoot }); const snapshotAfterRestore = cache.snapshot(); + // actions/cache/restore's cache-matched-key output, threaded in via the + // step env; empty on a cache miss. This makes the job log state plainly + // whether the Metro cache was actually restored. + const restoredKey = process.env.METRO_RESTORED_KEY; + if (restoredKey) { + const metroEntries = snapshotAfterRestore.metro ?? []; + const totalBytes = metroEntries.reduce( + (sum, entry) => sum + entry.sizeBytes, + 0 + ); + console.info( + `Metro cache: restored from key "${restoredKey}" (${metroEntries.length} files, ${formatMegabytes(totalBytes)}).` + ); + } else { + console.info( + 'Metro cache: no existing cache entry matched — starting cold.' + ); + } + const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`; fs.appendFileSync(githubOutput, output); From ae706a467ef799ae5ba6ae4debac24005e127c2a Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 19:16:31 +0200 Subject: [PATCH 6/9] ci: save Metro cache from PR runs in e2e workflow Set cacheSavePolicy: always on every react-native-harness action invocation so PR runs exercise the save path too, not only restores. --- .github/workflows/e2e-tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7b0076bf..7412faaf 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -97,6 +97,7 @@ jobs: app: android/app/build/outputs/apk/debug/app-debug.apk runner: android projectRoot: apps/playground + cacheSavePolicy: always preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" echo "HARNESS_RUNNER=$HARNESS_RUNNER" @@ -206,6 +207,7 @@ jobs: app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app runner: ios projectRoot: apps/playground + cacheSavePolicy: always preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" echo "HARNESS_RUNNER=$HARNESS_RUNNER" @@ -268,6 +270,7 @@ jobs: with: runner: chromium projectRoot: apps/playground + cacheSavePolicy: always preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" echo "HARNESS_RUNNER=$HARNESS_RUNNER" @@ -368,6 +371,7 @@ jobs: app: android/app/build/outputs/apk/debug/app-debug.apk runner: android-crash-pre-rn projectRoot: apps/playground + cacheSavePolicy: always harnessArgs: '--config jest.harness.crash.config.mjs --testPathPatterns src/__tests__/crash/android' preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" @@ -508,6 +512,7 @@ jobs: app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app runner: ios-crash-pre-rn projectRoot: apps/playground + cacheSavePolicy: always harnessArgs: '--config jest.harness.crash.config.mjs --testPathPatterns src/__tests__/crash/ios' preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" From ea272faed22e5de59c5f98d41d41bb4b26a76939 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 19:38:21 +0200 Subject: [PATCH 7/9] Revert "ci: save Metro cache from PR runs in e2e workflow" This reverts commit ae706a467ef799ae5ba6ae4debac24005e127c2a. --- .github/workflows/e2e-tests.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7412faaf..7b0076bf 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -97,7 +97,6 @@ jobs: app: android/app/build/outputs/apk/debug/app-debug.apk runner: android projectRoot: apps/playground - cacheSavePolicy: always preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" echo "HARNESS_RUNNER=$HARNESS_RUNNER" @@ -207,7 +206,6 @@ jobs: app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app runner: ios projectRoot: apps/playground - cacheSavePolicy: always preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" echo "HARNESS_RUNNER=$HARNESS_RUNNER" @@ -270,7 +268,6 @@ jobs: with: runner: chromium projectRoot: apps/playground - cacheSavePolicy: always preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" echo "HARNESS_RUNNER=$HARNESS_RUNNER" @@ -371,7 +368,6 @@ jobs: app: android/app/build/outputs/apk/debug/app-debug.apk runner: android-crash-pre-rn projectRoot: apps/playground - cacheSavePolicy: always harnessArgs: '--config jest.harness.crash.config.mjs --testPathPatterns src/__tests__/crash/android' preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" @@ -512,7 +508,6 @@ jobs: app: ios/build/Build/Products/Debug-iphonesimulator/HarnessPlayground.app runner: ios-crash-pre-rn projectRoot: apps/playground - cacheSavePolicy: always harnessArgs: '--config jest.harness.crash.config.mjs --testPathPatterns src/__tests__/crash/ios' preRunHook: | echo "HARNESS_PROJECT_ROOT=$HARNESS_PROJECT_ROOT" From b7618b10d1d2782b8b28ad867f8bf97971cf5feb Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 19:38:55 +0200 Subject: [PATCH 8/9] docs: clarify where Metro cache key files are discovered --- website/src/docs/guides/ci-cd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/docs/guides/ci-cd.md b/website/src/docs/guides/ci-cd.md index fa1f769a..982637e4 100644 --- a/website/src/docs/guides/ci-cd.md +++ b/website/src/docs/guides/ci-cd.md @@ -252,7 +252,7 @@ React Native Harness persists Metro's transform cache under `.harness/cache/metr When you use the `callstackincubator/react-native-harness` GitHub Action, Metro cache restoration and saving is handled automatically for the resolved `projectRoot`. Do not add your own `actions/cache` step for `.harness/cache/metro` or `.harness/cache/metro-file-map` -- the action's cache key scheme assumes it's the only thing writing and reading those entries, and a manually-added step would fight it for ownership of the same paths. -The cache key is computed by Harness itself, not a static file list: it hashes your lockfile(s) and Metro/Babel config together with the resolved `@react-native-harness/bundler-metro` version and your `cache.version` salt. This means the cache invalidates automatically whenever you upgrade `@react-native-harness/bundler-metro`, not only when a lockfile or config file changes. +The cache key is computed by Harness itself, not a static file list: it hashes your lockfile(s) and Metro/Babel config together with the resolved `@react-native-harness/bundler-metro` version and your `cache.version` salt. This means the cache invalidates automatically whenever you upgrade `@react-native-harness/bundler-metro`, not only when a lockfile or config file changes. Key files are discovered in `projectRoot` and its ancestor directories up to the repository root -- so in a monorepo the root lockfile is included, while sibling packages' Metro/Babel configs never affect your project's cache key. Use the `cacheSavePolicy` input to control when a new cache entry is saved: From 020ec9059846e04758e9914eb2ee7bd794f4ecdb Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 14 Jul 2026 22:03:36 +0200 Subject: [PATCH 9/9] test: raise playground testTimeout to 10s for slow CI UI interactions --- apps/playground/rn-harness.config.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/playground/rn-harness.config.mjs b/apps/playground/rn-harness.config.mjs index 1053d2f9..bb4badfe 100644 --- a/apps/playground/rn-harness.config.mjs +++ b/apps/playground/rn-harness.config.mjs @@ -116,6 +116,7 @@ export default { defaultRunner: 'android', platformReadyTimeout: 300000, bridgeTimeout: 120000, + testTimeout: 10000, permissions: true, detectNativeCrashes: true,