diff --git a/packages/middleware/package.json b/packages/middleware/package.json index c3226625..4b484852 100644 --- a/packages/middleware/package.json +++ b/packages/middleware/package.json @@ -38,7 +38,8 @@ "scripts": { "build": "vite build", "typecheck": "tsc -p tsconfig.lib.json --noEmit", - "lint": "eslint ." + "lint": "eslint .", + "test": "vitest --run" }, "dependencies": { "@rozenite/runtime": "workspace:*", @@ -48,6 +49,7 @@ "tslib": "^2.3.0" }, "devDependencies": { + "vitest": "^4.0.18", "@react-native/dev-middleware": "~0.76.0", "@types/ejs": "^3.1.5", "@types/express": "^5.0.3", diff --git a/packages/middleware/src/__tests__/auto-discovery.test.ts b/packages/middleware/src/__tests__/auto-discovery.test.ts new file mode 100644 index 00000000..9df1c590 --- /dev/null +++ b/packages/middleware/src/__tests__/auto-discovery.test.ts @@ -0,0 +1,250 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { findPackageRoot, getInstalledPlugins } from '../auto-discovery.js'; +import type { RozeniteConfig } from '../config.js'; + +const tempDirs: string[] = []; + +const createTempDir = (): string => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rozenite-middleware-')); + tempDirs.push(tempDir); + return tempDir; +}; + +const writeJson = (filePath: string, value: unknown): void => { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(value, null, 2)); +}; + +const writeFile = (filePath: string, contents = ''): void => { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, contents); +}; + +const createPackage = ( + packageRoot: string, + packageName: string, + options?: { + hasPluginManifest?: boolean; + main?: string; + } +): void => { + const main = options?.main ?? './dist/index.js'; + + writeJson(path.join(packageRoot, 'package.json'), { + name: packageName, + version: '1.0.0', + main, + }); + writeFile(path.join(packageRoot, main.replace('./', '')), 'module.exports = {};'); + + if (options?.hasPluginManifest) { + writeJson(path.join(packageRoot, 'dist', 'rozenite.json'), { + name: packageName, + }); + } +}; + +const createProject = ( + packageJson: Record +): string => { + const projectRoot = createTempDir(); + writeJson(path.join(projectRoot, 'package.json'), packageJson); + return projectRoot; +}; + +const createNodeModulesPackage = ( + projectRoot: string, + packageName: string, + options?: { + hasPluginManifest?: boolean; + main?: string; + } +): string => { + const packageRoot = path.join(projectRoot, 'node_modules', packageName); + createPackage(packageRoot, packageName, options); + return packageRoot; +}; + +const createConfig = ( + projectRoot: string, + overrides?: Partial +): RozeniteConfig => ({ + projectRoot, + ...overrides, +}); + +afterEach(() => { + while (tempDirs.length) { + fs.rmSync(tempDirs.pop()!, { recursive: true, force: true }); + } +}); + +describe('findPackageRoot', () => { + it('finds a package root for a classic node_modules path', () => { + const root = createTempDir(); + const packageRoot = path.join(root, 'node_modules', '@rozenite', 'demo-plugin'); + + createPackage(packageRoot, '@rozenite/demo-plugin', { + hasPluginManifest: true, + main: './dist/react-native.cjs', + }); + + const resolvedPath = path.join(packageRoot, 'dist', 'react-native.cjs'); + + expect(findPackageRoot('@rozenite/demo-plugin', resolvedPath)).toBe( + packageRoot + ); + }); + + it('finds a package root for a Yarn virtual or unplugged path', () => { + const root = createTempDir(); + const packageRoot = path.join( + root, + '.yarn', + '__virtual__', + 'demo-plugin-virtual-123', + '0', + 'cache', + 'demo-plugin-npm-1.0.0.zip', + 'node_modules', + 'demo-plugin' + ); + + createPackage(packageRoot, 'demo-plugin', { + hasPluginManifest: true, + }); + + const resolvedPath = path.join(packageRoot, 'dist', 'index.js'); + + expect(findPackageRoot('demo-plugin', resolvedPath)).toBe(packageRoot); + }); + + it('finds a package root for a workspace path without node_modules', () => { + const root = createTempDir(); + const packageRoot = path.join(root, 'packages', 'demo-plugin'); + + createPackage(packageRoot, 'demo-plugin', { + hasPluginManifest: true, + }); + + const resolvedPath = path.join(packageRoot, 'dist', 'index.js'); + + expect(findPackageRoot('demo-plugin', resolvedPath)).toBe(packageRoot); + }); +}); + +describe('getInstalledPlugins', () => { + it('includes declared dependencies that contain the manifest', () => { + const projectRoot = createProject({ + name: 'demo-app', + dependencies: { + 'demo-plugin': '1.0.0', + }, + }); + + const pluginRoot = createNodeModulesPackage(projectRoot, 'demo-plugin', { + hasPluginManifest: true, + }); + const resolvedPluginRoot = fs.realpathSync(pluginRoot); + + expect(getInstalledPlugins(createConfig(projectRoot))).toEqual([ + { + name: 'demo-plugin', + path: resolvedPluginRoot, + }, + ]); + }); + + it('ignores declared dependencies without the manifest', () => { + const projectRoot = createProject({ + name: 'demo-app', + dependencies: { + 'demo-plugin': '1.0.0', + }, + }); + + createNodeModulesPackage(projectRoot, 'demo-plugin'); + + expect(getInstalledPlugins(createConfig(projectRoot))).toEqual([]); + }); + + it('applies exclude before including a valid plugin', () => { + const projectRoot = createProject({ + name: 'demo-app', + dependencies: { + 'demo-plugin': '1.0.0', + }, + }); + + createNodeModulesPackage(projectRoot, 'demo-plugin', { + hasPluginManifest: true, + }); + + expect( + getInstalledPlugins( + createConfig(projectRoot, { exclude: ['demo-plugin'] }) + ) + ).toEqual([]); + }); + + it('deduplicates names declared in dependencies and devDependencies', () => { + const projectRoot = createProject({ + name: 'demo-app', + dependencies: { + 'demo-plugin': '1.0.0', + }, + devDependencies: { + 'demo-plugin': '1.0.0', + }, + }); + + const pluginRoot = createNodeModulesPackage(projectRoot, 'demo-plugin', { + hasPluginManifest: true, + }); + const resolvedPluginRoot = fs.realpathSync(pluginRoot); + + expect(getInstalledPlugins(createConfig(projectRoot))).toEqual([ + { + name: 'demo-plugin', + path: resolvedPluginRoot, + }, + ]); + }); + + it('skips unresolved dependencies without failing discovery', () => { + const projectRoot = createProject({ + name: 'demo-app', + dependencies: { + 'demo-plugin': '1.0.0', + 'missing-plugin': '1.0.0', + }, + }); + + const pluginRoot = createNodeModulesPackage(projectRoot, 'demo-plugin', { + hasPluginManifest: true, + }); + const resolvedPluginRoot = fs.realpathSync(pluginRoot); + + expect(getInstalledPlugins(createConfig(projectRoot))).toEqual([ + { + name: 'demo-plugin', + path: resolvedPluginRoot, + }, + ]); + }); + + it('throws in include mode when a plugin cannot be resolved', () => { + const projectRoot = createProject({ + name: 'demo-app', + }); + + expect(() => + getInstalledPlugins( + createConfig(projectRoot, { include: ['missing-plugin'] }) + ) + ).toThrowError('Could not resolve plugin missing-plugin.'); + }); +}); diff --git a/packages/middleware/src/auto-discovery.ts b/packages/middleware/src/auto-discovery.ts index 05e67383..799e5323 100644 --- a/packages/middleware/src/auto-discovery.ts +++ b/packages/middleware/src/auto-discovery.ts @@ -3,7 +3,6 @@ import path from 'node:path'; import assert from 'node:assert'; import { createRequire } from 'node:module'; import { logger } from './logger.js'; -import { getNodeModulesPaths } from './node-modules-paths.js'; import { ROZENITE_MANIFEST } from './constants.js'; import { RozeniteConfig } from './config.js'; @@ -14,42 +13,56 @@ export type InstalledPlugin = { path: string; }; -const isDirectoryOrSymlinkToDirectory = (filePath: string): boolean => { +const readPackageJsonName = (packagePath: string): string | null => { try { - fs.accessSync(filePath); + const packageJsonPath = path.join(packagePath, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return typeof packageJson.name === 'string' ? packageJson.name : null; } catch { - return false; + return null; } +}; - try { - const stats = fs.lstatSync(filePath); - - if (stats.isSymbolicLink()) { - const realPath = fs.realpathSync(filePath); - const realStats = fs.statSync(realPath); - return realStats.isDirectory(); - } else { - return stats.isDirectory(); +export const findPackageRoot = ( + packageName: string, + resolvedPath: string +): string | null => { + let currentPath = path.dirname(resolvedPath); + + while (true) { + if (readPackageJsonName(currentPath) === packageName) { + return currentPath; + } + + const parentPath = path.dirname(currentPath); + + if (parentPath === currentPath) { + return null; } - } catch (error) { - logger.warn(`Warning: Could not access ${filePath}:`, error); - return false; + + currentPath = parentPath; } }; -const tryResolvePlugin = ( +const resolvePackageRoot = ( projectRoot: string, - maybePlugin: string + packageName: string ): string | null => { try { - const pluginPath = require.resolve(maybePlugin, { paths: [projectRoot] }); - // lorem-ipsum/dist/index.js -> ../.. -> lorem-ipsum/ - return path.resolve(pluginPath, '..', '..'); + const resolvedPath = require.resolve(packageName, { paths: [projectRoot] }); + return findPackageRoot(packageName, resolvedPath); } catch { return null; } }; +const tryResolvePlugin = ( + projectRoot: string, + maybePlugin: string +): string | null => { + return resolvePackageRoot(projectRoot, maybePlugin); +}; + const getIncludedPlugins = (options: RozeniteConfig): InstalledPlugin[] => { assert(options.include, 'include is required'); @@ -85,107 +98,37 @@ export const getInstalledPlugins = ( return getIncludedPlugins(options); } - const nodeModulesPaths = getNodeModulesPaths(); + return getInstalledPluginsFromDependencies(options); +}; + +const getInstalledPluginsFromDependencies = ( + options: RozeniteConfig +): InstalledPlugin[] => { const plugins: InstalledPlugin[] = []; + const packageJsonPath = path.join(options.projectRoot, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + const dependencies = Array.from(new Set([ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.devDependencies || {}), + ])); - for (const nodeModulesPath of nodeModulesPaths) { - try { - fs.accessSync(nodeModulesPath); - } catch { + for (const dependency of dependencies) { + if (options.exclude?.includes(dependency)) { continue; } - try { - const entries = fs.readdirSync(nodeModulesPath, { - withFileTypes: true, - }); - - for (const entry of entries) { - if ( - !isDirectoryOrSymlinkToDirectory( - path.join(nodeModulesPath, entry.name) - ) - ) { - continue; - } - - const packageName = entry.name; - - if (packageName.startsWith('.')) { - continue; - } - - let packagePath: string; - let actualPackageName: string; - - if (packageName.startsWith('@')) { - const scopePath = path.join(nodeModulesPath, packageName); - - try { - fs.accessSync(scopePath); - } catch { - continue; - } - - try { - const scopedEntries = fs.readdirSync(scopePath, { - withFileTypes: true, - }); - - for (const scopedEntry of scopedEntries) { - if ( - !isDirectoryOrSymlinkToDirectory( - path.join(scopePath, scopedEntry.name) - ) - ) { - continue; - } - - packagePath = path.join(scopePath, scopedEntry.name); - actualPackageName = `${packageName}/${scopedEntry.name}`; - - const plugin = tryExtractPlugin(packagePath, actualPackageName); - - if ( - options.exclude && - options.exclude.includes(actualPackageName) - ) { - continue; - } - - if (plugin) { - plugins.push(plugin); - } - } - } catch (error) { - logger.warn( - `Warning: Could not read scope directory ${scopePath}:`, - error - ); - continue; - } - } else { - packagePath = path.join(nodeModulesPath, packageName); - actualPackageName = packageName; - - const plugin = tryExtractPlugin(packagePath, actualPackageName); - - if (options.exclude && options.exclude.includes(actualPackageName)) { - continue; - } - - if (plugin) { - plugins.push(plugin); - } - } - } - } catch (error) { - logger.warn( - `Warning: Could not read node_modules directory ${nodeModulesPath}:`, - error - ); + const packagePath = resolvePackageRoot(options.projectRoot, dependency); + + if (!packagePath) { continue; } + + const plugin = tryExtractPlugin(packagePath, dependency); + + if (plugin) { + plugins.push(plugin); + } } return plugins; diff --git a/packages/middleware/src/node-modules-paths.ts b/packages/middleware/src/node-modules-paths.ts deleted file mode 100644 index 78412500..00000000 --- a/packages/middleware/src/node-modules-paths.ts +++ /dev/null @@ -1,23 +0,0 @@ -import path from 'node:path'; - -export const getNodeModulesPaths = (): readonly string[] => { - const paths: string[] = []; - - // Add current working directory node_modules - paths.push(path.join(process.cwd(), 'node_modules')); - - // Add parent directories node_modules (for monorepos) - let currentDir = process.cwd(); - let parentDir = path.dirname(currentDir); - - while (parentDir !== currentDir) { - const parentNodeModules = path.join(parentDir, 'node_modules'); - if (parentNodeModules) { - paths.push(parentNodeModules); - } - currentDir = parentDir; - parentDir = path.dirname(currentDir); - } - - return paths; -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f59028bf..5ec3b2e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -552,6 +552,9 @@ importers: '@types/semver': specifier: ^7.7.0 version: 7.7.0 + vitest: + specifier: ^4.0.18 + version: 4.1.0(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) packages/mmkv-plugin: dependencies: @@ -2684,6 +2687,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.4': resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} @@ -4173,6 +4179,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -4849,6 +4858,9 @@ packages: '@vitest/expect@4.0.16': resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==} + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} + '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -4860,24 +4872,47 @@ packages: vite: optional: true + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} '@vitest/pretty-format@4.0.16': resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==} + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} '@vitest/spy@4.0.16': resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==} + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} + '@vitest/ui@3.2.4': resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} peerDependencies: @@ -4889,6 +4924,9 @@ packages: '@vitest/utils@4.0.16': resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==} + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} + '@volar/language-core@2.4.17': resolution: {integrity: sha512-chmRZMbKmcGpKMoO7Reb70uiLrzo0KWC2CkFttKUuKvrE+VYgi+fL9vWMJ07Fv5ulX0V1TAyyacN9q3nc5/ecA==} @@ -6196,6 +6234,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -6441,6 +6482,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expo-asset@55.0.4: resolution: {integrity: sha512-eaPPe9Sw4V0nL/KkEvuWpyeeSoGhP2fu1ZA7wkldqywhMVhhY+7kerMkQ7nPgJVtevIfkQRw7wD8ghZEzrKzmg==} peerDependencies: @@ -7975,6 +8020,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -8646,6 +8694,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -9979,6 +10030,9 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -10229,6 +10283,10 @@ packages: tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -10713,6 +10771,40 @@ packages: jsdom: optional: true + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vlq@0.2.3: resolution: {integrity: sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==} @@ -12995,6 +13087,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.4': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -15068,6 +15162,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} '@swc-node/core@1.14.1(@swc/core@1.5.29(@swc/helpers@0.5.17))(@swc/types@0.1.25)': @@ -15852,6 +15948,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 + '@vitest/expect@4.1.0': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.2 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + chai: 6.2.2 + tinyrainbow: 3.0.3 + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@18.16.9)(jiti@2.4.2)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -15860,6 +15965,14 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@18.16.9)(jiti@2.4.2)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 4.1.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -15868,24 +15981,42 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.1.0': + dependencies: + tinyrainbow: 3.0.3 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 strip-literal: 3.1.0 + '@vitest/runner@4.1.0': + dependencies: + '@vitest/utils': 4.1.0 + pathe: 2.0.3 + '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/snapshot@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.4 '@vitest/spy@4.0.16': {} + '@vitest/spy@4.1.0': {} + '@vitest/ui@3.2.4(vitest@3.2.4)': dependencies: '@vitest/utils': 3.2.4 @@ -15908,6 +16039,12 @@ snapshots: '@vitest/pretty-format': 4.0.16 tinyrainbow: 3.0.3 + '@vitest/utils@4.1.0': + dependencies: + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.0.3 + '@volar/language-core@2.4.17': dependencies: '@volar/source-map': 2.4.17 @@ -17408,6 +17545,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -17796,6 +17935,8 @@ snapshots: expect-type@1.2.2: {} + expect-type@1.3.0: {} + expo-asset@55.0.4(expo@55.0.0-preview.10)(react-native@0.83.1(@babel/core@7.28.0)(@react-native/metro-config@0.76.9)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: '@expo/image-utils': 0.8.12 @@ -19635,6 +19776,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.4 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@2.1.0: dependencies: pify: 4.0.1 @@ -20859,6 +21004,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + on-exit-leak-free@2.1.2: {} on-finished@2.3.0: @@ -22463,6 +22610,8 @@ snapshots: std-env@3.9.0: {} + std-env@4.0.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -22754,6 +22903,8 @@ snapshots: tinyexec@1.0.1: {} + tinyexec@1.0.4: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) @@ -23302,6 +23453,45 @@ snapshots: - tsx - yaml + vitest@4.1.0(@types/node@22.17.0)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@22.1.0)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1): + dependencies: + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.0.4 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@22.17.0)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.17.0 + '@vitest/ui': 3.2.4(vitest@3.2.4) + jsdom: 22.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vlq@0.2.3: {} vlq@1.0.1: {}