From bd0b0b405068579ea2d1f2b81a2f7303cabe49cc Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Jun 2025 23:42:43 +0200 Subject: [PATCH 01/21] chore: bootstrap resolver cases --- pnpm-lock.yaml | 83 ++++++++ tests/resolver-cases/README.md | 92 +++++++++ tests/resolver-cases/package.json | 18 ++ .../src/__tests__/asset-resolution.test.ts | 68 +++++++ .../src/__tests__/exports-resolution.test.ts | 181 ++++++++++++++++++ .../src/__tests__/platform-resolution.test.ts | 113 +++++++++++ tests/resolver-cases/src/resolver.ts | 113 +++++++++++ tests/resolver-cases/src/setup.ts | 95 +++++++++ tests/resolver-cases/src/virtual-fs.ts | 94 +++++++++ tests/resolver-cases/tsconfig.json | 18 ++ tests/resolver-cases/vitest.config.ts | 8 + 11 files changed, 883 insertions(+) create mode 100644 tests/resolver-cases/README.md create mode 100644 tests/resolver-cases/package.json create mode 100644 tests/resolver-cases/src/__tests__/asset-resolution.test.ts create mode 100644 tests/resolver-cases/src/__tests__/exports-resolution.test.ts create mode 100644 tests/resolver-cases/src/__tests__/platform-resolution.test.ts create mode 100644 tests/resolver-cases/src/resolver.ts create mode 100644 tests/resolver-cases/src/setup.ts create mode 100644 tests/resolver-cases/src/virtual-fs.ts create mode 100644 tests/resolver-cases/tsconfig.json create mode 100644 tests/resolver-cases/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a3d868f6..165309267 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -706,6 +706,27 @@ importers: specifier: ^4.12.0 version: 4.22.1 + tests/resolver-cases: + devDependencies: + '@callstack/repack': + specifier: workspace:* + version: link:../../packages/repack + '@types/node': + specifier: 'catalog:' + version: 18.19.41 + enhanced-resolve: + specifier: ^5.17.1 + version: 5.18.1 + memfs: + specifier: ^4.13.0 + version: 4.17.0 + typescript: + specifier: 'catalog:' + version: 5.8.3 + vitest: + specifier: ^2.0.5 + version: 2.0.5(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3) + website: dependencies: '@callstack/rspress-theme': @@ -17143,6 +17164,24 @@ snapshots: unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 + vite-node@2.0.5(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.3(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@2.0.5(@types/node@20.14.11)(lightningcss@1.28.2)(terser@5.31.3): dependencies: cac: 6.7.14 @@ -17161,6 +17200,17 @@ snapshots: - supports-color - terser + vite@5.4.3(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.1 + rollup: 4.21.2 + optionalDependencies: + '@types/node': 18.19.41 + fsevents: 2.3.3 + lightningcss: 1.28.2 + terser: 5.31.3 + vite@5.4.3(@types/node@20.14.11)(lightningcss@1.28.2)(terser@5.31.3): dependencies: esbuild: 0.21.5 @@ -17172,6 +17222,39 @@ snapshots: lightningcss: 1.28.2 terser: 5.31.3 + vitest@2.0.5(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.4.0 + execa: 8.0.1 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.9.0 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.4.3(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3) + vite-node: 2.0.5(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.41 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.0.5(@types/node@20.14.11)(lightningcss@1.28.2)(terser@5.31.3): dependencies: '@ampproject/remapping': 2.3.0 diff --git a/tests/resolver-cases/README.md b/tests/resolver-cases/README.md new file mode 100644 index 000000000..f0a68bcd3 --- /dev/null +++ b/tests/resolver-cases/README.md @@ -0,0 +1,92 @@ +# Resolver Test Cases + +This package provides a testing framework for Repack's module resolution logic. It allows testing various edge cases and scenarios that packages in the React Native ecosystem might present. + +## Features + +- **Virtual File System**: Test resolution without creating real files using `memfs` +- **Repack Integration**: Uses `getResolveOptions` from Repack with `enhanced-resolve` +- **Platform-specific Resolution**: Test iOS, Android, and web platform file resolution +- **Package Exports Support**: Test modern package.json `exports` field resolution +- **Asset Resolution**: Test resolution of scaled assets (@2x, @3x) +- **TypeScript Support**: Full TypeScript support with proper types + +## Architecture + +### Core Components + +- **VirtualFileSystem**: Creates in-memory filesystems for testing +- **RepackResolver**: Bridges Repack's resolve options with enhanced-resolve +- **Package Templates**: Predefined package structures for common patterns +- **Test Utilities**: Helper functions for setting up test environments + +### Bridging byDependency + +Since `enhanced-resolve` doesn't support Webpack's `byDependency` option directly, this package merges dependency-specific condition names into the main resolver configuration. + +## Usage + +```typescript +import { + setupTestEnvironment, + resolveFromApp, +} from "./src/test-utils/setup.js"; +import { reactStrictDomTemplate } from "./src/utils/package-templates.js"; + +// Set up a test environment with a virtual package +const context = await setupTestEnvironment( + [{ name: "react-strict-dom", package: reactStrictDomTemplate }], + { platform: "ios", enablePackageExports: true } +); + +// Test resolution +const result = await resolveFromApp(context, "react-strict-dom"); +expect(result).toBe("/node_modules/react-strict-dom/dist/native/index.js"); +``` + +## Test Categories + +### Platform Resolution + +- iOS/Android platform-specific files (`.ios.js`, `.android.js`) +- Native fallbacks (`.native.js`) +- TypeScript platform extensions (`.ios.ts`, `.android.ts`) + +### Package Exports + +- Conditional exports with `react-native` condition +- ESM vs CommonJS resolution +- Subpath exports + +### Asset Resolution + +- Scaled asset resolution (@2x, @3x) +- Different asset formats (PNG, JPG, MP4) + +## Running Tests + +```bash +pnpm test +``` + +For watch mode: + +```bash +pnpm test:watch +``` + +## Adding New Test Cases + +1. **Create Package Template**: Add new templates to `src/utils/package-templates.ts` +2. **Write Tests**: Create test files in `src/__tests__/` +3. **Use Test Utilities**: Leverage `setupTestEnvironment` and `resolveFromApp` helpers + +## Package Templates + +The package includes several predefined templates representing common patterns: + +- **reactStrictDomTemplate**: React Strict DOM with conditional exports +- **platformSpecificTemplate**: Traditional platform-specific files +- **typescriptPlatformTemplate**: TypeScript with platform extensions +- **complexExportsTemplate**: Complex exports with import/require conditions +- **assetResolutionTemplate**: Asset files with scale factors diff --git a/tests/resolver-cases/package.json b/tests/resolver-cases/package.json new file mode 100644 index 000000000..4a2757388 --- /dev/null +++ b/tests/resolver-cases/package.json @@ -0,0 +1,18 @@ +{ + "name": "@repack/resolver-cases", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@callstack/repack": "workspace:*", + "@types/node": "catalog:", + "enhanced-resolve": "^5.17.1", + "memfs": "^4.13.0", + "typescript": "catalog:", + "vitest": "^2.0.5" + } +} diff --git a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts new file mode 100644 index 000000000..1a0c591f8 --- /dev/null +++ b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, test } from 'vitest'; +import { resolveFromApp, setupTestEnvironment } from '../setup.js'; +import type { VirtualPackage } from '../virtual-fs.js'; + +// Template for asset resolution testing +const assetResolutionTemplate: VirtualPackage = { + name: 'asset-lib', + version: '1.0.0', + packageJson: { + name: 'asset-lib', + version: '1.0.0', + main: './index.js', + }, + files: { + 'index.js': 'export const assets = require("./assets");', + 'assets/icon.png': 'fake-png-content', + 'assets/icon@2x.png': 'fake-png-content-2x', + 'assets/icon@3x.png': 'fake-png-content-3x', + 'assets/logo.jpg': 'fake-jpg-content', + 'assets/logo@2x.jpg': 'fake-jpg-content-2x', + 'assets/video.mp4': 'fake-mp4-content', + }, +}; + +describe('Asset Resolution', () => { + test('should resolve base asset when no scale specified', async () => { + const context = await setupTestEnvironment( + [{ name: 'asset-lib', package: assetResolutionTemplate }], + { platform: 'ios' } + ); + + const result = await resolveFromApp(context, 'asset-lib/assets/icon.png'); + expect(result).toBe('/node_modules/asset-lib/assets/icon.png'); + }); + + test('should resolve 2x scaled assets', async () => { + const context = await setupTestEnvironment( + [{ name: 'asset-lib', package: assetResolutionTemplate }], + { platform: 'ios' } + ); + + // This would typically be handled by extensionAlias in getResolveOptions + const result = await resolveFromApp( + context, + 'asset-lib/assets/icon@2x.png' + ); + expect(result).toBe('/node_modules/asset-lib/assets/icon@2x.png'); + }); + + test('should resolve different asset formats', async () => { + const context = await setupTestEnvironment( + [{ name: 'asset-lib', package: assetResolutionTemplate }], + { platform: 'ios' } + ); + + const jpgResult = await resolveFromApp( + context, + 'asset-lib/assets/logo.jpg' + ); + expect(jpgResult).toBe('/node_modules/asset-lib/assets/logo.jpg'); + + const mp4Result = await resolveFromApp( + context, + 'asset-lib/assets/video.mp4' + ); + expect(mp4Result).toBe('/node_modules/asset-lib/assets/video.mp4'); + }); +}); diff --git a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts new file mode 100644 index 000000000..bb08d30cc --- /dev/null +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, test } from 'vitest'; +import { resolveFromApp, setupTestEnvironment } from '../setup.js'; +import type { VirtualPackage } from '../virtual-fs.js'; + +// Template for react-strict-dom package with conditional exports +const reactStrictDomTemplate: VirtualPackage = { + name: 'react-strict-dom', + version: '0.0.36', + packageJson: { + name: 'react-strict-dom', + version: '0.0.36', + description: 'React Strict DOM', + exports: { + '.': { + 'react-native': { + types: './dist/native/index.d.ts', + default: './dist/native/index.js', + }, + default: { + types: './dist/dom/index.d.ts', + default: './dist/dom/index.js', + }, + }, + './babel-preset': './babel/preset.js', + './runtime': './dist/dom/runtime.js', + './package.json': './package.json', + }, + }, + files: { + 'dist/native/index.js': 'export const platform = "native";', + 'dist/native/index.d.ts': 'export declare const platform: "native";', + 'dist/dom/index.js': 'export const platform = "dom";', + 'dist/dom/index.d.ts': 'export declare const platform: "dom";', + 'dist/dom/runtime.js': 'export const runtime = "dom";', + 'babel/preset.js': 'module.exports = {};', + }, +}; + +// Template for a package with complex exports field +const complexExportsTemplate: VirtualPackage = { + name: 'complex-exports-lib', + version: '1.0.0', + packageJson: { + name: 'complex-exports-lib', + version: '1.0.0', + exports: { + '.': { + import: { + 'react-native': './esm/index.native.js', + default: './esm/index.js', + }, + require: { + 'react-native': './cjs/index.native.js', + default: './cjs/index.js', + }, + }, + './utils': { + import: './esm/utils.js', + require: './cjs/utils.js', + }, + './native': { + 'react-native': './native/index.js', + }, + }, + }, + files: { + 'esm/index.js': + 'export const format = "esm"; export const platform = "web";', + 'esm/index.native.js': + 'export const format = "esm"; export const platform = "native";', + 'esm/utils.js': 'export const utils = "esm";', + 'cjs/index.js': 'exports.format = "cjs"; exports.platform = "web";', + 'cjs/index.native.js': + 'exports.format = "cjs"; exports.platform = "native";', + 'cjs/utils.js': 'exports.utils = "cjs";', + 'native/index.js': 'export const platform = "native-only";', + }, +}; + +describe('Package Exports Resolution', () => { + describe('React Strict DOM pattern', () => { + test('should resolve to native version for react-native condition', async () => { + const context = await setupTestEnvironment( + [{ name: 'react-strict-dom', package: reactStrictDomTemplate }], + { platform: 'ios', enablePackageExports: true } + ); + + const result = await resolveFromApp(context, 'react-strict-dom'); + expect(result).toBe( + '/node_modules/react-strict-dom/dist/native/index.js' + ); + }); + + test('should resolve to DOM version when package exports disabled', async () => { + const context = await setupTestEnvironment( + [{ name: 'react-strict-dom', package: reactStrictDomTemplate }], + { platform: 'ios', enablePackageExports: false } + ); + + // Without package exports, should fall back to main field resolution + const result = await resolveFromApp(context, 'react-strict-dom'); + expect(result).toBe(null); // No main field in this package + }); + + test('should resolve subpath exports', async () => { + const context = await setupTestEnvironment( + [{ name: 'react-strict-dom', package: reactStrictDomTemplate }], + { platform: 'ios', enablePackageExports: true } + ); + + const runtimeResult = await resolveFromApp( + context, + 'react-strict-dom/runtime' + ); + expect(runtimeResult).toBe( + '/node_modules/react-strict-dom/dist/dom/runtime.js' + ); + + const babelResult = await resolveFromApp( + context, + 'react-strict-dom/babel-preset' + ); + expect(babelResult).toBe( + '/node_modules/react-strict-dom/babel/preset.js' + ); + }); + }); + + describe('Complex exports patterns', () => { + test('should resolve ESM imports with react-native condition', async () => { + const context = await setupTestEnvironment( + [{ name: 'complex-lib', package: complexExportsTemplate }], + { platform: 'ios', enablePackageExports: true } + ); + + const result = await resolveFromApp(context, 'complex-lib', 'esm'); + expect(result).toBe('/node_modules/complex-lib/esm/index.native.js'); + }); + + test('should resolve CommonJS requires with react-native condition', async () => { + const context = await setupTestEnvironment( + [{ name: 'complex-lib', package: complexExportsTemplate }], + { platform: 'ios', enablePackageExports: true } + ); + + const result = await resolveFromApp(context, 'complex-lib', 'commonjs'); + expect(result).toBe('/node_modules/complex-lib/cjs/index.native.js'); + }); + + test('should resolve utils subpath', async () => { + const context = await setupTestEnvironment( + [{ name: 'complex-lib', package: complexExportsTemplate }], + { platform: 'ios', enablePackageExports: true } + ); + + const esmResult = await resolveFromApp( + context, + 'complex-lib/utils', + 'esm' + ); + expect(esmResult).toBe('/node_modules/complex-lib/esm/utils.js'); + + const cjsResult = await resolveFromApp( + context, + 'complex-lib/utils', + 'commonjs' + ); + expect(cjsResult).toBe('/node_modules/complex-lib/cjs/utils.js'); + }); + + test('should resolve react-native only exports', async () => { + const context = await setupTestEnvironment( + [{ name: 'complex-lib', package: complexExportsTemplate }], + { platform: 'ios', enablePackageExports: true } + ); + + const result = await resolveFromApp(context, 'complex-lib/native'); + expect(result).toBe('/node_modules/complex-lib/native/index.js'); + }); + }); +}); diff --git a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts new file mode 100644 index 000000000..67542f296 --- /dev/null +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, test } from 'vitest'; +import { resolveFromApp, setupTestEnvironment } from '../setup.js'; +import type { VirtualPackage } from '../virtual-fs.js'; + +// Template for a package with platform-specific files +const platformSpecificTemplate: VirtualPackage = { + name: 'platform-specific-lib', + version: '1.0.0', + packageJson: { + name: 'platform-specific-lib', + version: '1.0.0', + main: './index.js', + 'react-native': './index.native.js', + }, + files: { + 'index.js': 'export const platform = "web";', + 'index.native.js': 'export const platform = "native";', + 'index.ios.js': 'export const platform = "ios";', + 'index.android.js': 'export const platform = "android";', + 'lib/utils.js': 'export const utils = "web";', + 'lib/utils.native.js': 'export const utils = "native";', + 'lib/utils.ios.js': 'export const utils = "ios";', + 'lib/utils.android.js': 'export const utils = "android";', + }, +}; + +// Template for a package with TypeScript platform extensions +const typescriptPlatformTemplate: VirtualPackage = { + name: 'typescript-platform-lib', + version: '1.0.0', + packageJson: { + name: 'typescript-platform-lib', + version: '1.0.0', + main: './dist/index.js', + types: './dist/index.d.ts', + }, + files: { + 'dist/index.js': 'export const platform = "web";', + 'dist/index.d.ts': 'export declare const platform: "web";', + 'dist/index.native.js': 'export const platform = "native";', + 'dist/index.native.d.ts': 'export declare const platform: "native";', + 'dist/index.ios.js': 'export const platform = "ios";', + 'dist/index.ios.d.ts': 'export declare const platform: "ios";', + 'dist/index.android.js': 'export const platform = "android";', + 'dist/index.android.d.ts': 'export declare const platform: "android";', + 'src/utils.ts': 'export const utils = "web";', + 'src/utils.native.ts': 'export const utils = "native";', + 'src/utils.ios.ts': 'export const utils = "ios";', + 'src/utils.android.ts': 'export const utils = "android";', + }, +}; + +describe('Platform Resolution', () => { + test('should resolve iOS platform files when platform is ios', async () => { + const context = await setupTestEnvironment( + [{ name: 'platform-lib', package: platformSpecificTemplate }], + { platform: 'ios', preferNativePlatform: true } + ); + + const result = await resolveFromApp(context, 'platform-lib'); + expect(result).toBe('/node_modules/platform-lib/index.ios.js'); + }); + + test('should resolve Android platform files when platform is android', async () => { + const context = await setupTestEnvironment( + [{ name: 'platform-lib', package: platformSpecificTemplate }], + { platform: 'android', preferNativePlatform: true } + ); + + const result = await resolveFromApp(context, 'platform-lib'); + expect(result).toBe('/node_modules/platform-lib/index.android.js'); + }); + + test('should fallback to native when platform file not found', async () => { + const context = await setupTestEnvironment( + [{ name: 'platform-lib', package: platformSpecificTemplate }], + { platform: 'web', preferNativePlatform: true } + ); + + const result = await resolveFromApp(context, 'platform-lib'); + expect(result).toBe('/node_modules/platform-lib/index.native.js'); + }); + + test('should resolve platform-specific TypeScript files', async () => { + const context = await setupTestEnvironment( + [{ name: 'ts-platform-lib', package: typescriptPlatformTemplate }], + { platform: 'ios', preferNativePlatform: true } + ); + + const result = await resolveFromApp(context, 'ts-platform-lib/src/utils'); + expect(result).toBe('/node_modules/ts-platform-lib/src/utils.ios.ts'); + }); + + test('should prefer platform over native when preferNativePlatform is false', async () => { + const context = await setupTestEnvironment( + [{ name: 'platform-lib', package: platformSpecificTemplate }], + { platform: 'ios', preferNativePlatform: false } + ); + + const result = await resolveFromApp(context, 'platform-lib'); + expect(result).toBe('/node_modules/platform-lib/index.ios.js'); + }); + + test('should resolve nested platform-specific files', async () => { + const context = await setupTestEnvironment( + [{ name: 'platform-lib', package: platformSpecificTemplate }], + { platform: 'android', preferNativePlatform: true } + ); + + const result = await resolveFromApp(context, 'platform-lib/lib/utils'); + expect(result).toBe('/node_modules/platform-lib/lib/utils.android.js'); + }); +}); diff --git a/tests/resolver-cases/src/resolver.ts b/tests/resolver-cases/src/resolver.ts new file mode 100644 index 000000000..fdc174711 --- /dev/null +++ b/tests/resolver-cases/src/resolver.ts @@ -0,0 +1,113 @@ +import { type ResolveOptions, getResolveOptions } from '@callstack/repack'; +import { type Resolver, ResolverFactory } from 'enhanced-resolve'; +import type { VirtualFileSystem } from './virtual-fs.js'; + +export interface RepackResolverOptions extends ResolveOptions { + platform?: string; + fileSystem?: any; +} + +export class RepackResolver { + private resolvers: Map = new Map(); + private options: RepackResolverOptions; + + constructor(options: RepackResolverOptions = {}) { + this.options = options; + } + + /** + * Creates a resolver for a specific dependency type (esm/commonjs) + */ + private createResolver(dependencyType: 'esm' | 'commonjs'): Resolver { + const platform = this.options.platform || 'ios'; + const resolveOptions = getResolveOptions(platform, { + enablePackageExports: this.options.enablePackageExports, + preferNativePlatform: this.options.preferNativePlatform, + }); + + // Enhanced-resolve doesn't support byDependency directly, + // so we need to merge the dependency-specific options + const dependencyOptions = resolveOptions.byDependency[dependencyType] || {}; + const mergedConditionNames = [ + ...resolveOptions.conditionNames, + ...dependencyOptions.conditionNames, + ]; + + const enhancedResolveOptions = { + mainFields: resolveOptions.mainFields, + aliasFields: resolveOptions.aliasFields, + conditionNames: mergedConditionNames, + exportsFields: resolveOptions.exportsFields, + extensions: resolveOptions.extensions, + extensionAlias: resolveOptions.extensionAlias, + fileSystem: this.options.fileSystem, + // Enhanced-resolve specific options + cache: false, // Disable caching for tests + symlinks: false, + }; + + return ResolverFactory.createResolver(enhancedResolveOptions); + } + + /** + * Get resolver for a specific dependency type + */ + getResolver(dependencyType: 'esm' | 'commonjs' = 'esm'): Resolver { + if (!this.resolvers.has(dependencyType)) { + this.resolvers.set(dependencyType, this.createResolver(dependencyType)); + } + return this.resolvers.get(dependencyType)!; + } + + /** + * Resolve a module with ESM semantics + */ + async resolveESM(context: string, request: string): Promise { + const resolver = this.getResolver('esm'); + return new Promise((resolve, reject) => { + resolver.resolve({}, context, request, {}, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result as string); + } + }); + }); + } + + /** + * Resolve a module with CommonJS semantics + */ + async resolveCommonJS( + context: string, + request: string + ): Promise { + const resolver = this.getResolver('commonjs'); + return new Promise((resolve, reject) => { + resolver.resolve({}, context, request, {}, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result as string); + } + }); + }); + } + + /** + * Update the resolver options (useful for testing different configurations) + */ + updateOptions(newOptions: Partial): void { + this.options = { ...this.options, ...newOptions }; + // Clear cached resolvers so they get recreated with new options + this.resolvers.clear(); + } + + /** + * Set a virtual file system for testing + */ + setFileSystem(vfs: VirtualFileSystem): void { + this.options.fileSystem = vfs.getFileSystem(); + this.resolvers.clear(); + } +} diff --git a/tests/resolver-cases/src/setup.ts b/tests/resolver-cases/src/setup.ts new file mode 100644 index 000000000..64ff18873 --- /dev/null +++ b/tests/resolver-cases/src/setup.ts @@ -0,0 +1,95 @@ +import { RepackResolver, type RepackResolverOptions } from './resolver.js'; +import { VirtualFileSystem, type VirtualPackage } from './virtual-fs.js'; + +/** + * Test context for resolver tests + */ +export interface ResolverTestContext { + vfs: VirtualFileSystem; + resolver: RepackResolver; + nodeModulesPath: string; +} + +/** + * Creates a test context with virtual filesystem and resolver + */ +export function createResolverTestContext( + options: RepackResolverOptions = {} +): ResolverTestContext { + const vfs = new VirtualFileSystem(); + const resolver = new RepackResolver(options); + resolver.setFileSystem(vfs); + + return { + vfs, + resolver, + nodeModulesPath: '/node_modules', + }; +} + +/** + * Sets up a test environment with packages + */ +export async function setupTestEnvironment( + packages: Array<{ name: string; package: VirtualPackage }>, + options: RepackResolverOptions = {} +): Promise { + const context = createResolverTestContext(options); + + // Create all packages in node_modules + const packagePromises = packages.map(({ name, package: pkg }) => + context.vfs.createPackage(`${context.nodeModulesPath}/${name}`, pkg) + ); + + await Promise.all(packagePromises); + + return context; +} + +/** + * Helper to create a minimal test app structure + */ +export async function createTestApp( + context: ResolverTestContext, + appFiles: Record = {} +): Promise { + // Create app directory + await context.vfs.createPackage('/app', { + name: 'test-app', + version: '1.0.0', + packageJson: { + name: 'test-app', + version: '1.0.0', + main: './index.js', + }, + files: { + 'index.js': 'console.log("Hello, world!");', + ...appFiles, + }, + }); +} + +/** + * Helper for testing resolution from app context + */ +export async function resolveFromApp( + context: ResolverTestContext, + request: string, + dependencyType: 'esm' | 'commonjs' = 'esm' +): Promise { + const appContext = '/app'; + + if (dependencyType === 'esm') { + return context.resolver.resolveESM(appContext, request); + } + return context.resolver.resolveCommonJS(appContext, request); +} + +/** + * Helper to debug resolution by listing all files + */ +export function debugVirtualFs(context: ResolverTestContext): void { + console.log('Virtual filesystem contents:'); + const files = context.vfs.listFiles(); + files.forEach((file) => console.log(` ${file}`)); +} diff --git a/tests/resolver-cases/src/virtual-fs.ts b/tests/resolver-cases/src/virtual-fs.ts new file mode 100644 index 000000000..feca823b2 --- /dev/null +++ b/tests/resolver-cases/src/virtual-fs.ts @@ -0,0 +1,94 @@ +import { Volume } from 'memfs'; +import type { IFs } from 'memfs'; + +export interface VirtualPackage { + name: string; + version: string; + packageJson: Record; + files: Record; +} + +export class VirtualFileSystem { + private volume: InstanceType; + public fs: IFs; + + constructor() { + this.volume = new Volume(); + this.fs = this.volume.promises as any; + } + + /** + * Creates a virtual package in the filesystem + */ + async createPackage(packagePath: string, pkg: VirtualPackage): Promise { + const basePath = packagePath.endsWith('/') + ? packagePath + : `${packagePath}/`; + + // Create package.json + const packageJsonPath = `${basePath}package.json`; + await this.volume.promises.mkdir(basePath, { recursive: true }); + await this.volume.promises.writeFile( + packageJsonPath, + JSON.stringify(pkg.packageJson, null, 2) + ); + + // Create all other files + for (const [filePath, content] of Object.entries(pkg.files)) { + const fullPath = `${basePath}${filePath}`; + const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/')); + + if (dirPath !== basePath.slice(0, -1)) { + await this.volume.promises.mkdir(dirPath, { recursive: true }); + } + + await this.volume.promises.writeFile(fullPath, content); + } + } + + /** + * Creates multiple packages at once + */ + async createPackages( + packages: Array<{ path: string; package: VirtualPackage }> + ): Promise { + await Promise.all( + packages.map(({ path, package: pkg }) => this.createPackage(path, pkg)) + ); + } + + /** + * Gets the underlying memfs instance for use with enhanced-resolve + */ + getFileSystem(): InstanceType { + return this.volume; + } + + /** + * Lists all files in the virtual filesystem (useful for debugging) + */ + listFiles(): string[] { + const files: string[] = []; + const volume = this.volume; + + function walk(dir: string): void { + try { + const items = volume.readdirSync(dir) as string[]; + for (const item of items) { + const fullPath = `${dir}/${item}`; + const stat = volume.statSync(fullPath); + if (stat.isDirectory()) { + walk(fullPath); + } else { + files.push(fullPath); + } + } + } catch { + // Ignore errors + } + } + + walk('/'); + return files; + } +} diff --git a/tests/resolver-cases/tsconfig.json b/tests/resolver-cases/tsconfig.json new file mode 100644 index 000000000..a3fcd4de1 --- /dev/null +++ b/tests/resolver-cases/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ESNext"], + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "noEmit": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src/**/*", "**/*.test.ts"], + "exclude": ["node_modules"] +} diff --git a/tests/resolver-cases/vitest.config.ts b/tests/resolver-cases/vitest.config.ts new file mode 100644 index 000000000..12d866d6f --- /dev/null +++ b/tests/resolver-cases/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + testTimeout: 10000, + }, +}); From 4d853ee244140b20c55485512cef2abbeaa490b6 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Thu, 26 Jun 2025 23:56:20 +0200 Subject: [PATCH 02/21] chore: trim the fat --- tests/resolver-cases/src/resolver.ts | 59 ++++++-------------------- tests/resolver-cases/src/virtual-fs.ts | 20 --------- 2 files changed, 12 insertions(+), 67 deletions(-) diff --git a/tests/resolver-cases/src/resolver.ts b/tests/resolver-cases/src/resolver.ts index fdc174711..5c1a8c9e1 100644 --- a/tests/resolver-cases/src/resolver.ts +++ b/tests/resolver-cases/src/resolver.ts @@ -15,9 +15,6 @@ export class RepackResolver { this.options = options; } - /** - * Creates a resolver for a specific dependency type (esm/commonjs) - */ private createResolver(dependencyType: 'esm' | 'commonjs'): Resolver { const platform = this.options.platform || 'ios'; const resolveOptions = getResolveOptions(platform, { @@ -49,65 +46,33 @@ export class RepackResolver { return ResolverFactory.createResolver(enhancedResolveOptions); } - /** - * Get resolver for a specific dependency type - */ - getResolver(dependencyType: 'esm' | 'commonjs' = 'esm'): Resolver { + setFileSystem(vfs: VirtualFileSystem): void { + this.options.fileSystem = vfs.getFileSystem(); + this.resolvers.clear(); + } + + getOrCreateResolver(dependencyType: 'esm' | 'commonjs' = 'esm'): Resolver { if (!this.resolvers.has(dependencyType)) { this.resolvers.set(dependencyType, this.createResolver(dependencyType)); } return this.resolvers.get(dependencyType)!; } - /** - * Resolve a module with ESM semantics - */ - async resolveESM(context: string, request: string): Promise { - const resolver = this.getResolver('esm'); + async resolveESM(context: string, request: string): Promise { + const resolver = this.getOrCreateResolver('esm'); return new Promise((resolve, reject) => { resolver.resolve({}, context, request, {}, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result as string); - } + return err ? reject(err) : resolve(result as string); }); }); } - /** - * Resolve a module with CommonJS semantics - */ - async resolveCommonJS( - context: string, - request: string - ): Promise { - const resolver = this.getResolver('commonjs'); + async resolveCommonJS(context: string, request: string): Promise { + const resolver = this.getOrCreateResolver('commonjs'); return new Promise((resolve, reject) => { resolver.resolve({}, context, request, {}, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result as string); - } + return err ? reject(err) : resolve(result as string); }); }); } - - /** - * Update the resolver options (useful for testing different configurations) - */ - updateOptions(newOptions: Partial): void { - this.options = { ...this.options, ...newOptions }; - // Clear cached resolvers so they get recreated with new options - this.resolvers.clear(); - } - - /** - * Set a virtual file system for testing - */ - setFileSystem(vfs: VirtualFileSystem): void { - this.options.fileSystem = vfs.getFileSystem(); - this.resolvers.clear(); - } } diff --git a/tests/resolver-cases/src/virtual-fs.ts b/tests/resolver-cases/src/virtual-fs.ts index feca823b2..12de31165 100644 --- a/tests/resolver-cases/src/virtual-fs.ts +++ b/tests/resolver-cases/src/virtual-fs.ts @@ -17,9 +17,6 @@ export class VirtualFileSystem { this.fs = this.volume.promises as any; } - /** - * Creates a virtual package in the filesystem - */ async createPackage(packagePath: string, pkg: VirtualPackage): Promise { const basePath = packagePath.endsWith('/') ? packagePath @@ -46,27 +43,10 @@ export class VirtualFileSystem { } } - /** - * Creates multiple packages at once - */ - async createPackages( - packages: Array<{ path: string; package: VirtualPackage }> - ): Promise { - await Promise.all( - packages.map(({ path, package: pkg }) => this.createPackage(path, pkg)) - ); - } - - /** - * Gets the underlying memfs instance for use with enhanced-resolve - */ getFileSystem(): InstanceType { return this.volume; } - /** - * Lists all files in the virtual filesystem (useful for debugging) - */ listFiles(): string[] { const files: string[] = []; const volume = this.volume; From 213370be5eb9d1ec8c5122a833069fd36e5aa16b Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:12:57 +0200 Subject: [PATCH 03/21] fix: tests --- tests/resolver-cases/README.md | 142 +++++---- .../src/__tests__/asset-resolution.test.ts | 92 +++--- .../src/__tests__/exports-resolution.test.ts | 283 +++++++++++------- .../src/__tests__/platform-resolution.test.ts | 163 +++++----- tests/resolver-cases/src/resolver.ts | 78 ----- tests/resolver-cases/src/setup.ts | 95 ------ tests/resolver-cases/src/test-helpers.ts | 122 ++++++++ tests/resolver-cases/src/virtual-fs.ts | 74 ----- 8 files changed, 520 insertions(+), 529 deletions(-) delete mode 100644 tests/resolver-cases/src/resolver.ts delete mode 100644 tests/resolver-cases/src/setup.ts create mode 100644 tests/resolver-cases/src/test-helpers.ts delete mode 100644 tests/resolver-cases/src/virtual-fs.ts diff --git a/tests/resolver-cases/README.md b/tests/resolver-cases/README.md index f0a68bcd3..a2f1dc7ac 100644 --- a/tests/resolver-cases/README.md +++ b/tests/resolver-cases/README.md @@ -1,92 +1,122 @@ # Resolver Test Cases -This package provides a testing framework for Repack's module resolution logic. It allows testing various edge cases and scenarios that packages in the React Native ecosystem might present. +A simple testing framework for Repack's module resolution logic. Tests various edge cases and scenarios from the React Native ecosystem without unnecessary abstractions. -## Features +## Philosophy: Simple and Direct -- **Virtual File System**: Test resolution without creating real files using `memfs` -- **Repack Integration**: Uses `getResolveOptions` from Repack with `enhanced-resolve` -- **Platform-specific Resolution**: Test iOS, Android, and web platform file resolution -- **Package Exports Support**: Test modern package.json `exports` field resolution -- **Asset Resolution**: Test resolution of scaled assets (@2x, @3x) -- **TypeScript Support**: Full TypeScript support with proper types +This package follows a "senior engineer" approach: -## Architecture +- **Zero abstractions**: Direct file structure mapping +- **Visible test data**: Everything you need is right in the test +- **Clear intent**: When you read a test, you know exactly what files exist +- **No duplication**: No repeated fields or interface overhead -### Core Components +## Quick Start -- **VirtualFileSystem**: Creates in-memory filesystems for testing -- **RepackResolver**: Bridges Repack's resolve options with enhanced-resolve -- **Package Templates**: Predefined package structures for common patterns -- **Test Utilities**: Helper functions for setting up test environments +```typescript +import { setupTestEnvironment } from "../test-helpers.js"; + +// Set up packages as simple file structures +const { resolve } = await setupTestEnvironment( + { + "my-package": { + "package.json": JSON.stringify({ + name: "my-package", + exports: { + ".": { + "react-native": "./native.js", + default: "./web.js", + }, + }, + }), + "native.js": 'export const platform = "native";', + "web.js": 'export const platform = "web";', + }, + }, + { platform: "ios", enablePackageExports: true } +); -### Bridging byDependency +const result = await resolve("my-package"); +expect(result).toBe("/node_modules/my-package/native.js"); +``` -Since `enhanced-resolve` doesn't support Webpack's `byDependency` option directly, this package merges dependency-specific condition names into the main resolver configuration. +## What You See Is What You Get -## Usage +When reading a test, you can immediately see: + +- What packages exist +- What their `package.json` contains +- What files they have and their content +- No hidden templates or interfaces ```typescript -import { - setupTestEnvironment, - resolveFromApp, -} from "./src/test-utils/setup.js"; -import { reactStrictDomTemplate } from "./src/utils/package-templates.js"; - -// Set up a test environment with a virtual package -const context = await setupTestEnvironment( - [{ name: "react-strict-dom", package: reactStrictDomTemplate }], - { platform: "ios", enablePackageExports: true } +const { resolve } = await setupTestEnvironment( + { + "react-lib": { + "package.json": JSON.stringify({ name: "react-lib", main: "./index.js" }), + "index.js": 'export const platform = "web";', + "index.ios.js": 'export const platform = "ios";', + "index.android.js": 'export const platform = "android";', + }, + }, + { platform: "ios" } ); - -// Test resolution -const result = await resolveFromApp(context, "react-strict-dom"); -expect(result).toBe("/node_modules/react-strict-dom/dist/native/index.js"); ``` -## Test Categories +## What's Tested ### Platform Resolution -- iOS/Android platform-specific files (`.ios.js`, `.android.js`) +- Platform-specific files (`.ios.js`, `.android.js`) - Native fallbacks (`.native.js`) -- TypeScript platform extensions (`.ios.ts`, `.android.ts`) +- TypeScript platform extensions +- `preferNativePlatform` behavior ### Package Exports - Conditional exports with `react-native` condition -- ESM vs CommonJS resolution +- ESM vs CommonJS resolution differences - Subpath exports ### Asset Resolution -- Scaled asset resolution (@2x, @3x) -- Different asset formats (PNG, JPG, MP4) +- Scaled asset handling (@2x, @3x) +- Extension alias behavior -## Running Tests +## API -```bash -pnpm test -``` +### `setupTestEnvironment(packages, options)` -For watch mode: +Creates a virtual filesystem with packages and returns a resolver. -```bash -pnpm test:watch -``` +**Parameters:** + +- `packages` - Object where keys are package names, values are file structures +- `options` - Resolver options (platform, enablePackageExports, etc.) + +**Returns:** -## Adding New Test Cases +- `resolve(request, context?, dependencyType?)` - Resolve a module request +- `volume` - Direct access to memfs Volume +- `listFiles()` - Debug helper to see all files -1. **Create Package Template**: Add new templates to `src/utils/package-templates.ts` -2. **Write Tests**: Create test files in `src/__tests__/` -3. **Use Test Utilities**: Leverage `setupTestEnvironment` and `resolveFromApp` helpers +## Structure + +``` +src/ +├── test-helpers.ts # Single helper file with everything +└── __tests__/ + ├── platform-resolution.test.ts + ├── exports-resolution.test.ts + └── asset-resolution.test.ts +``` -## Package Templates +## Why This Approach? -The package includes several predefined templates representing common patterns: +1. **No Interfaces**: Package structure is just `Record>` +2. **No Duplication**: No repeated name/version fields +3. **Immediate Understanding**: Test setup == actual filesystem structure +4. **Easy Debugging**: `listFiles()` shows exactly what you created +5. **Copy-Paste Friendly**: Easy to copy real package.json content -- **reactStrictDomTemplate**: React Strict DOM with conditional exports -- **platformSpecificTemplate**: Traditional platform-specific files -- **typescriptPlatformTemplate**: TypeScript with platform extensions -- **complexExportsTemplate**: Complex exports with import/require conditions -- **assetResolutionTemplate**: Asset files with scale factors +When a test fails, you know exactly what files exist and what they contain, all visible right in the test. diff --git a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts index 1a0c591f8..150631725 100644 --- a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts @@ -1,68 +1,72 @@ import { describe, expect, test } from 'vitest'; -import { resolveFromApp, setupTestEnvironment } from '../setup.js'; -import type { VirtualPackage } from '../virtual-fs.js'; - -// Template for asset resolution testing -const assetResolutionTemplate: VirtualPackage = { - name: 'asset-lib', - version: '1.0.0', - packageJson: { - name: 'asset-lib', - version: '1.0.0', - main: './index.js', - }, - files: { - 'index.js': 'export const assets = require("./assets");', - 'assets/icon.png': 'fake-png-content', - 'assets/icon@2x.png': 'fake-png-content-2x', - 'assets/icon@3x.png': 'fake-png-content-3x', - 'assets/logo.jpg': 'fake-jpg-content', - 'assets/logo@2x.jpg': 'fake-jpg-content-2x', - 'assets/video.mp4': 'fake-mp4-content', - }, -}; +import { setupTestEnvironment } from '../test-helpers.js'; describe('Asset Resolution', () => { - test('should resolve base asset when no scale specified', async () => { - const context = await setupTestEnvironment( - [{ name: 'asset-lib', package: assetResolutionTemplate }], + test('should resolve base asset when no scale specified and no scaled versions exist', async () => { + const { resolve } = await setupTestEnvironment( + { + 'asset-lib': { + 'package.json': JSON.stringify({ + name: 'asset-lib', + version: '1.0.0', + main: './index.js', + }), + 'index.js': 'export const assets = require("./assets");', + 'assets/icon.png': 'fake-png-content', + 'assets/logo.jpg': 'fake-jpg-content', + 'assets/video.mp4': 'fake-mp4-content', + }, + }, { platform: 'ios' } ); - const result = await resolveFromApp(context, 'asset-lib/assets/icon.png'); + const result = await resolve('asset-lib/assets/icon.png'); expect(result).toBe('/node_modules/asset-lib/assets/icon.png'); }); - test('should resolve 2x scaled assets', async () => { - const context = await setupTestEnvironment( - [{ name: 'asset-lib', package: assetResolutionTemplate }], + test('should resolve 2x scaled assets when available', async () => { + const { resolve } = await setupTestEnvironment( + { + 'asset-lib': { + 'package.json': JSON.stringify({ + name: 'asset-lib', + version: '1.0.0', + main: './index.js', + }), + 'index.js': 'export const assets = require("./assets");', + 'assets/icon.png': 'fake-png-content', + 'assets/icon@2x.png': 'fake-png-content-2x', + 'assets/icon@3x.png': 'fake-png-content-3x', + }, + }, { platform: 'ios' } ); - // This would typically be handled by extensionAlias in getResolveOptions - const result = await resolveFromApp( - context, - 'asset-lib/assets/icon@2x.png' - ); + // React Native prefers scaled assets when available + const result = await resolve('asset-lib/assets/icon.png'); expect(result).toBe('/node_modules/asset-lib/assets/icon@2x.png'); }); test('should resolve different asset formats', async () => { - const context = await setupTestEnvironment( - [{ name: 'asset-lib', package: assetResolutionTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'asset-lib': { + 'package.json': JSON.stringify({ + name: 'asset-lib', + version: '1.0.0', + main: './index.js', + }), + 'assets/logo.jpg': 'fake-jpg-content', + 'assets/video.mp4': 'fake-mp4-content', + }, + }, { platform: 'ios' } ); - const jpgResult = await resolveFromApp( - context, - 'asset-lib/assets/logo.jpg' - ); + const jpgResult = await resolve('asset-lib/assets/logo.jpg'); expect(jpgResult).toBe('/node_modules/asset-lib/assets/logo.jpg'); - const mp4Result = await resolveFromApp( - context, - 'asset-lib/assets/video.mp4' - ); + const mp4Result = await resolve('asset-lib/assets/video.mp4'); expect(mp4Result).toBe('/node_modules/asset-lib/assets/video.mp4'); }); }); diff --git a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts index bb08d30cc..d82f93c79 100644 --- a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -1,125 +1,108 @@ import { describe, expect, test } from 'vitest'; -import { resolveFromApp, setupTestEnvironment } from '../setup.js'; -import type { VirtualPackage } from '../virtual-fs.js'; - -// Template for react-strict-dom package with conditional exports -const reactStrictDomTemplate: VirtualPackage = { - name: 'react-strict-dom', - version: '0.0.36', - packageJson: { - name: 'react-strict-dom', - version: '0.0.36', - description: 'React Strict DOM', - exports: { - '.': { - 'react-native': { - types: './dist/native/index.d.ts', - default: './dist/native/index.js', - }, - default: { - types: './dist/dom/index.d.ts', - default: './dist/dom/index.js', - }, - }, - './babel-preset': './babel/preset.js', - './runtime': './dist/dom/runtime.js', - './package.json': './package.json', - }, - }, - files: { - 'dist/native/index.js': 'export const platform = "native";', - 'dist/native/index.d.ts': 'export declare const platform: "native";', - 'dist/dom/index.js': 'export const platform = "dom";', - 'dist/dom/index.d.ts': 'export declare const platform: "dom";', - 'dist/dom/runtime.js': 'export const runtime = "dom";', - 'babel/preset.js': 'module.exports = {};', - }, -}; - -// Template for a package with complex exports field -const complexExportsTemplate: VirtualPackage = { - name: 'complex-exports-lib', - version: '1.0.0', - packageJson: { - name: 'complex-exports-lib', - version: '1.0.0', - exports: { - '.': { - import: { - 'react-native': './esm/index.native.js', - default: './esm/index.js', - }, - require: { - 'react-native': './cjs/index.native.js', - default: './cjs/index.js', - }, - }, - './utils': { - import: './esm/utils.js', - require: './cjs/utils.js', - }, - './native': { - 'react-native': './native/index.js', - }, - }, - }, - files: { - 'esm/index.js': - 'export const format = "esm"; export const platform = "web";', - 'esm/index.native.js': - 'export const format = "esm"; export const platform = "native";', - 'esm/utils.js': 'export const utils = "esm";', - 'cjs/index.js': 'exports.format = "cjs"; exports.platform = "web";', - 'cjs/index.native.js': - 'exports.format = "cjs"; exports.platform = "native";', - 'cjs/utils.js': 'exports.utils = "cjs";', - 'native/index.js': 'export const platform = "native-only";', - }, -}; +import { setupTestEnvironment } from '../test-helpers.js'; describe('Package Exports Resolution', () => { describe('React Strict DOM pattern', () => { test('should resolve to native version for react-native condition', async () => { - const context = await setupTestEnvironment( - [{ name: 'react-strict-dom', package: reactStrictDomTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'react-strict-dom': { + 'package.json': JSON.stringify({ + name: 'react-strict-dom', + version: '0.0.36', + description: 'React Strict DOM', + exports: { + '.': { + 'react-native': { + types: './dist/native/index.d.ts', + default: './dist/native/index.js', + }, + default: { + types: './dist/dom/index.d.ts', + default: './dist/dom/index.js', + }, + }, + './babel-preset': './babel/preset.js', + './runtime': './dist/dom/runtime.js', + './package.json': './package.json', + }, + }), + 'dist/native/index.js': 'export const platform = "native";', + 'dist/native/index.d.ts': + 'export declare const platform: "native";', + 'dist/dom/index.js': 'export const platform = "dom";', + 'dist/dom/index.d.ts': 'export declare const platform: "dom";', + 'dist/dom/runtime.js': 'export const runtime = "dom";', + 'babel/preset.js': 'module.exports = {};', + }, + }, { platform: 'ios', enablePackageExports: true } ); - const result = await resolveFromApp(context, 'react-strict-dom'); + const result = await resolve('react-strict-dom'); expect(result).toBe( '/node_modules/react-strict-dom/dist/native/index.js' ); }); test('should resolve to DOM version when package exports disabled', async () => { - const context = await setupTestEnvironment( - [{ name: 'react-strict-dom', package: reactStrictDomTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'react-strict-dom': { + 'package.json': JSON.stringify({ + name: 'react-strict-dom', + version: '0.0.36', + description: 'React Strict DOM', + exports: { + '.': { + 'react-native': { + types: './dist/native/index.d.ts', + default: './dist/native/index.js', + }, + default: { + types: './dist/dom/index.d.ts', + default: './dist/dom/index.js', + }, + }, + }, + }), + 'dist/native/index.js': 'export const platform = "native";', + 'dist/dom/index.js': 'export const platform = "dom";', + }, + }, { platform: 'ios', enablePackageExports: false } ); // Without package exports, should fall back to main field resolution - const result = await resolveFromApp(context, 'react-strict-dom'); + const result = await resolve('react-strict-dom'); expect(result).toBe(null); // No main field in this package }); test('should resolve subpath exports', async () => { - const context = await setupTestEnvironment( - [{ name: 'react-strict-dom', package: reactStrictDomTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'react-strict-dom': { + 'package.json': JSON.stringify({ + name: 'react-strict-dom', + version: '0.0.36', + exports: { + './babel-preset': './babel/preset.js', + './runtime': './dist/dom/runtime.js', + }, + }), + 'dist/dom/runtime.js': 'export const runtime = "dom";', + 'babel/preset.js': 'module.exports = {};', + }, + }, { platform: 'ios', enablePackageExports: true } ); - const runtimeResult = await resolveFromApp( - context, - 'react-strict-dom/runtime' - ); + const runtimeResult = await resolve('react-strict-dom/runtime'); expect(runtimeResult).toBe( '/node_modules/react-strict-dom/dist/dom/runtime.js' ); - const babelResult = await resolveFromApp( - context, - 'react-strict-dom/babel-preset' - ); + const babelResult = await resolve('react-strict-dom/babel-preset'); expect(babelResult).toBe( '/node_modules/react-strict-dom/babel/preset.js' ); @@ -128,53 +111,125 @@ describe('Package Exports Resolution', () => { describe('Complex exports patterns', () => { test('should resolve ESM imports with react-native condition', async () => { - const context = await setupTestEnvironment( - [{ name: 'complex-lib', package: complexExportsTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'complex-lib': { + 'package.json': JSON.stringify({ + name: 'complex-exports-lib', + version: '1.0.0', + exports: { + '.': { + import: { + 'react-native': './esm/index.native.js', + default: './esm/index.js', + }, + require: { + 'react-native': './cjs/index.native.js', + default: './cjs/index.js', + }, + }, + }, + }), + 'esm/index.js': + 'export const format = "esm"; export const platform = "web";', + 'esm/index.native.js': + 'export const format = "esm"; export const platform = "native";', + 'cjs/index.js': 'exports.format = "cjs"; exports.platform = "web";', + 'cjs/index.native.js': + 'exports.format = "cjs"; exports.platform = "native";', + }, + }, { platform: 'ios', enablePackageExports: true } ); - const result = await resolveFromApp(context, 'complex-lib', 'esm'); + const result = await resolve('complex-lib', '/app', 'esm'); expect(result).toBe('/node_modules/complex-lib/esm/index.native.js'); }); test('should resolve CommonJS requires with react-native condition', async () => { - const context = await setupTestEnvironment( - [{ name: 'complex-lib', package: complexExportsTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'complex-lib': { + 'package.json': JSON.stringify({ + name: 'complex-exports-lib', + version: '1.0.0', + exports: { + '.': { + import: { + 'react-native': './esm/index.native.js', + default: './esm/index.js', + }, + require: { + 'react-native': './cjs/index.native.js', + default: './cjs/index.js', + }, + }, + }, + }), + 'esm/index.js': + 'export const format = "esm"; export const platform = "web";', + 'esm/index.native.js': + 'export const format = "esm"; export const platform = "native";', + 'cjs/index.js': 'exports.format = "cjs"; exports.platform = "web";', + 'cjs/index.native.js': + 'exports.format = "cjs"; exports.platform = "native";', + }, + }, { platform: 'ios', enablePackageExports: true } ); - const result = await resolveFromApp(context, 'complex-lib', 'commonjs'); + const result = await resolve('complex-lib', '/app', 'commonjs'); expect(result).toBe('/node_modules/complex-lib/cjs/index.native.js'); }); test('should resolve utils subpath', async () => { - const context = await setupTestEnvironment( - [{ name: 'complex-lib', package: complexExportsTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'complex-lib': { + 'package.json': JSON.stringify({ + name: 'complex-exports-lib', + version: '1.0.0', + exports: { + './utils': { + import: './esm/utils.js', + require: './cjs/utils.js', + }, + }, + }), + 'esm/utils.js': 'export const utils = "esm";', + 'cjs/utils.js': 'exports.utils = "cjs";', + }, + }, { platform: 'ios', enablePackageExports: true } ); - const esmResult = await resolveFromApp( - context, - 'complex-lib/utils', - 'esm' - ); + const esmResult = await resolve('complex-lib/utils', '/app', 'esm'); expect(esmResult).toBe('/node_modules/complex-lib/esm/utils.js'); - const cjsResult = await resolveFromApp( - context, - 'complex-lib/utils', - 'commonjs' - ); + const cjsResult = await resolve('complex-lib/utils', '/app', 'commonjs'); expect(cjsResult).toBe('/node_modules/complex-lib/cjs/utils.js'); }); test('should resolve react-native only exports', async () => { - const context = await setupTestEnvironment( - [{ name: 'complex-lib', package: complexExportsTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'complex-lib': { + 'package.json': JSON.stringify({ + name: 'complex-exports-lib', + version: '1.0.0', + exports: { + './native': { + 'react-native': './native/index.js', + }, + }, + }), + 'native/index.js': 'export const platform = "native-only";', + }, + }, { platform: 'ios', enablePackageExports: true } ); - const result = await resolveFromApp(context, 'complex-lib/native'); + const result = await resolve('complex-lib/native'); expect(result).toBe('/node_modules/complex-lib/native/index.js'); }); }); diff --git a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts index 67542f296..7061198b3 100644 --- a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -1,113 +1,140 @@ import { describe, expect, test } from 'vitest'; -import { resolveFromApp, setupTestEnvironment } from '../setup.js'; -import type { VirtualPackage } from '../virtual-fs.js'; - -// Template for a package with platform-specific files -const platformSpecificTemplate: VirtualPackage = { - name: 'platform-specific-lib', - version: '1.0.0', - packageJson: { - name: 'platform-specific-lib', - version: '1.0.0', - main: './index.js', - 'react-native': './index.native.js', - }, - files: { - 'index.js': 'export const platform = "web";', - 'index.native.js': 'export const platform = "native";', - 'index.ios.js': 'export const platform = "ios";', - 'index.android.js': 'export const platform = "android";', - 'lib/utils.js': 'export const utils = "web";', - 'lib/utils.native.js': 'export const utils = "native";', - 'lib/utils.ios.js': 'export const utils = "ios";', - 'lib/utils.android.js': 'export const utils = "android";', - }, -}; - -// Template for a package with TypeScript platform extensions -const typescriptPlatformTemplate: VirtualPackage = { - name: 'typescript-platform-lib', - version: '1.0.0', - packageJson: { - name: 'typescript-platform-lib', - version: '1.0.0', - main: './dist/index.js', - types: './dist/index.d.ts', - }, - files: { - 'dist/index.js': 'export const platform = "web";', - 'dist/index.d.ts': 'export declare const platform: "web";', - 'dist/index.native.js': 'export const platform = "native";', - 'dist/index.native.d.ts': 'export declare const platform: "native";', - 'dist/index.ios.js': 'export const platform = "ios";', - 'dist/index.ios.d.ts': 'export declare const platform: "ios";', - 'dist/index.android.js': 'export const platform = "android";', - 'dist/index.android.d.ts': 'export declare const platform: "android";', - 'src/utils.ts': 'export const utils = "web";', - 'src/utils.native.ts': 'export const utils = "native";', - 'src/utils.ios.ts': 'export const utils = "ios";', - 'src/utils.android.ts': 'export const utils = "android";', - }, -}; +import { setupTestEnvironment } from '../test-helpers.js'; describe('Platform Resolution', () => { test('should resolve iOS platform files when platform is ios', async () => { - const context = await setupTestEnvironment( - [{ name: 'platform-lib', package: platformSpecificTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'platform-lib': { + 'package.json': JSON.stringify({ + name: 'platform-specific-lib', + version: '1.0.0', + main: './index', + }), + 'index.js': 'export const platform = "web";', + 'index.native.js': 'export const platform = "native";', + 'index.ios.js': 'export const platform = "ios";', + 'index.android.js': 'export const platform = "android";', + }, + }, { platform: 'ios', preferNativePlatform: true } ); - const result = await resolveFromApp(context, 'platform-lib'); + const result = await resolve('platform-lib'); expect(result).toBe('/node_modules/platform-lib/index.ios.js'); }); test('should resolve Android platform files when platform is android', async () => { - const context = await setupTestEnvironment( - [{ name: 'platform-lib', package: platformSpecificTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'platform-lib': { + 'package.json': JSON.stringify({ + name: 'platform-specific-lib', + version: '1.0.0', + main: './index', + }), + 'index.js': 'export const platform = "web";', + 'index.native.js': 'export const platform = "native";', + 'index.ios.js': 'export const platform = "ios";', + 'index.android.js': 'export const platform = "android";', + }, + }, { platform: 'android', preferNativePlatform: true } ); - const result = await resolveFromApp(context, 'platform-lib'); + const result = await resolve('platform-lib'); expect(result).toBe('/node_modules/platform-lib/index.android.js'); }); test('should fallback to native when platform file not found', async () => { - const context = await setupTestEnvironment( - [{ name: 'platform-lib', package: platformSpecificTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'platform-lib': { + 'package.json': JSON.stringify({ + name: 'platform-specific-lib', + version: '1.0.0', + main: './index', + }), + 'index.js': 'export const platform = "web";', + 'index.native.js': 'export const platform = "native";', + 'index.ios.js': 'export const platform = "ios";', + 'index.android.js': 'export const platform = "android";', + }, + }, { platform: 'web', preferNativePlatform: true } ); - const result = await resolveFromApp(context, 'platform-lib'); + const result = await resolve('platform-lib'); expect(result).toBe('/node_modules/platform-lib/index.native.js'); }); test('should resolve platform-specific TypeScript files', async () => { - const context = await setupTestEnvironment( - [{ name: 'ts-platform-lib', package: typescriptPlatformTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'ts-platform-lib': { + 'package.json': JSON.stringify({ + name: 'typescript-platform-lib', + version: '1.0.0', + main: './dist/index.js', + types: './dist/index.d.ts', + }), + 'dist/index.js': 'export const platform = "web";', + 'dist/index.d.ts': 'export declare const platform: "web";', + 'src/utils.ts': 'export const utils = "web";', + 'src/utils.native.ts': 'export const utils = "native";', + 'src/utils.ios.ts': 'export const utils = "ios";', + 'src/utils.android.ts': 'export const utils = "android";', + }, + }, { platform: 'ios', preferNativePlatform: true } ); - const result = await resolveFromApp(context, 'ts-platform-lib/src/utils'); + const result = await resolve('ts-platform-lib/src/utils'); expect(result).toBe('/node_modules/ts-platform-lib/src/utils.ios.ts'); }); test('should prefer platform over native when preferNativePlatform is false', async () => { - const context = await setupTestEnvironment( - [{ name: 'platform-lib', package: platformSpecificTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'platform-lib': { + 'package.json': JSON.stringify({ + name: 'platform-specific-lib', + version: '1.0.0', + main: './index', + }), + 'index.js': 'export const platform = "web";', + 'index.native.js': 'export const platform = "native";', + 'index.ios.js': 'export const platform = "ios";', + 'index.android.js': 'export const platform = "android";', + }, + }, { platform: 'ios', preferNativePlatform: false } ); - const result = await resolveFromApp(context, 'platform-lib'); + const result = await resolve('platform-lib'); expect(result).toBe('/node_modules/platform-lib/index.ios.js'); }); test('should resolve nested platform-specific files', async () => { - const context = await setupTestEnvironment( - [{ name: 'platform-lib', package: platformSpecificTemplate }], + const { resolve } = await setupTestEnvironment( + { + 'platform-lib': { + 'package.json': JSON.stringify({ + name: 'platform-specific-lib', + version: '1.0.0', + main: './index', + }), + 'index.js': 'export const platform = "web";', + 'lib/utils.js': 'export const utils = "web";', + 'lib/utils.native.js': 'export const utils = "native";', + 'lib/utils.ios.js': 'export const utils = "ios";', + 'lib/utils.android.js': 'export const utils = "android";', + }, + }, { platform: 'android', preferNativePlatform: true } ); - const result = await resolveFromApp(context, 'platform-lib/lib/utils'); + const result = await resolve('platform-lib/lib/utils'); expect(result).toBe('/node_modules/platform-lib/lib/utils.android.js'); }); }); diff --git a/tests/resolver-cases/src/resolver.ts b/tests/resolver-cases/src/resolver.ts deleted file mode 100644 index 5c1a8c9e1..000000000 --- a/tests/resolver-cases/src/resolver.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { type ResolveOptions, getResolveOptions } from '@callstack/repack'; -import { type Resolver, ResolverFactory } from 'enhanced-resolve'; -import type { VirtualFileSystem } from './virtual-fs.js'; - -export interface RepackResolverOptions extends ResolveOptions { - platform?: string; - fileSystem?: any; -} - -export class RepackResolver { - private resolvers: Map = new Map(); - private options: RepackResolverOptions; - - constructor(options: RepackResolverOptions = {}) { - this.options = options; - } - - private createResolver(dependencyType: 'esm' | 'commonjs'): Resolver { - const platform = this.options.platform || 'ios'; - const resolveOptions = getResolveOptions(platform, { - enablePackageExports: this.options.enablePackageExports, - preferNativePlatform: this.options.preferNativePlatform, - }); - - // Enhanced-resolve doesn't support byDependency directly, - // so we need to merge the dependency-specific options - const dependencyOptions = resolveOptions.byDependency[dependencyType] || {}; - const mergedConditionNames = [ - ...resolveOptions.conditionNames, - ...dependencyOptions.conditionNames, - ]; - - const enhancedResolveOptions = { - mainFields: resolveOptions.mainFields, - aliasFields: resolveOptions.aliasFields, - conditionNames: mergedConditionNames, - exportsFields: resolveOptions.exportsFields, - extensions: resolveOptions.extensions, - extensionAlias: resolveOptions.extensionAlias, - fileSystem: this.options.fileSystem, - // Enhanced-resolve specific options - cache: false, // Disable caching for tests - symlinks: false, - }; - - return ResolverFactory.createResolver(enhancedResolveOptions); - } - - setFileSystem(vfs: VirtualFileSystem): void { - this.options.fileSystem = vfs.getFileSystem(); - this.resolvers.clear(); - } - - getOrCreateResolver(dependencyType: 'esm' | 'commonjs' = 'esm'): Resolver { - if (!this.resolvers.has(dependencyType)) { - this.resolvers.set(dependencyType, this.createResolver(dependencyType)); - } - return this.resolvers.get(dependencyType)!; - } - - async resolveESM(context: string, request: string): Promise { - const resolver = this.getOrCreateResolver('esm'); - return new Promise((resolve, reject) => { - resolver.resolve({}, context, request, {}, (err, result) => { - return err ? reject(err) : resolve(result as string); - }); - }); - } - - async resolveCommonJS(context: string, request: string): Promise { - const resolver = this.getOrCreateResolver('commonjs'); - return new Promise((resolve, reject) => { - resolver.resolve({}, context, request, {}, (err, result) => { - return err ? reject(err) : resolve(result as string); - }); - }); - } -} diff --git a/tests/resolver-cases/src/setup.ts b/tests/resolver-cases/src/setup.ts deleted file mode 100644 index 64ff18873..000000000 --- a/tests/resolver-cases/src/setup.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { RepackResolver, type RepackResolverOptions } from './resolver.js'; -import { VirtualFileSystem, type VirtualPackage } from './virtual-fs.js'; - -/** - * Test context for resolver tests - */ -export interface ResolverTestContext { - vfs: VirtualFileSystem; - resolver: RepackResolver; - nodeModulesPath: string; -} - -/** - * Creates a test context with virtual filesystem and resolver - */ -export function createResolverTestContext( - options: RepackResolverOptions = {} -): ResolverTestContext { - const vfs = new VirtualFileSystem(); - const resolver = new RepackResolver(options); - resolver.setFileSystem(vfs); - - return { - vfs, - resolver, - nodeModulesPath: '/node_modules', - }; -} - -/** - * Sets up a test environment with packages - */ -export async function setupTestEnvironment( - packages: Array<{ name: string; package: VirtualPackage }>, - options: RepackResolverOptions = {} -): Promise { - const context = createResolverTestContext(options); - - // Create all packages in node_modules - const packagePromises = packages.map(({ name, package: pkg }) => - context.vfs.createPackage(`${context.nodeModulesPath}/${name}`, pkg) - ); - - await Promise.all(packagePromises); - - return context; -} - -/** - * Helper to create a minimal test app structure - */ -export async function createTestApp( - context: ResolverTestContext, - appFiles: Record = {} -): Promise { - // Create app directory - await context.vfs.createPackage('/app', { - name: 'test-app', - version: '1.0.0', - packageJson: { - name: 'test-app', - version: '1.0.0', - main: './index.js', - }, - files: { - 'index.js': 'console.log("Hello, world!");', - ...appFiles, - }, - }); -} - -/** - * Helper for testing resolution from app context - */ -export async function resolveFromApp( - context: ResolverTestContext, - request: string, - dependencyType: 'esm' | 'commonjs' = 'esm' -): Promise { - const appContext = '/app'; - - if (dependencyType === 'esm') { - return context.resolver.resolveESM(appContext, request); - } - return context.resolver.resolveCommonJS(appContext, request); -} - -/** - * Helper to debug resolution by listing all files - */ -export function debugVirtualFs(context: ResolverTestContext): void { - console.log('Virtual filesystem contents:'); - const files = context.vfs.listFiles(); - files.forEach((file) => console.log(` ${file}`)); -} diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts new file mode 100644 index 000000000..545bda6ff --- /dev/null +++ b/tests/resolver-cases/src/test-helpers.ts @@ -0,0 +1,122 @@ +import { type ResolveOptions, getResolveOptions } from '@callstack/repack'; +import { ResolverFactory } from 'enhanced-resolve'; +import { Volume } from 'memfs'; + +export interface TestOptions extends ResolveOptions { + platform?: string; +} + +// Simple function to create a package in the virtual filesystem +async function createPackage( + volume: InstanceType, + packagePath: string, + files: Record +): Promise { + const basePath = packagePath.endsWith('/') ? packagePath : `${packagePath}/`; + + // Ensure the package directory exists + await volume.promises.mkdir(basePath, { recursive: true }); + + // Create all files (including package.json) + for (const [filePath, content] of Object.entries(files)) { + const fullPath = `${basePath}${filePath}`; + const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/')); + + // Create intermediate directories if needed + if (dirPath !== basePath.slice(0, -1)) { + await volume.promises.mkdir(dirPath, { recursive: true }); + } + + await volume.promises.writeFile(fullPath, content); + } +} + +// Main setup function - creates filesystem and resolver +export async function setupTestEnvironment( + packages: Record>, + options: TestOptions = {} +) { + const volume = new Volume(); + const platform = options.platform || 'ios'; + + // Ensure node_modules directory exists first + await volume.promises.mkdir('/node_modules', { recursive: true }); + + // Create all packages in node_modules + for (const [packageName, files] of Object.entries(packages)) { + await createPackage(volume, `/node_modules/${packageName}`, files); + } + + // Get resolve options from Repack + const resolveOptions = getResolveOptions(platform, { + enablePackageExports: options.enablePackageExports, + preferNativePlatform: options.preferNativePlatform, + }); + + // Create resolvers for both ESM and CommonJS + const createResolver = (dependencyType: 'esm' | 'commonjs') => { + const dependencyOptions = resolveOptions.byDependency[dependencyType] || {}; + const mergedConditionNames = [ + ...resolveOptions.conditionNames, + ...dependencyOptions.conditionNames, + ]; + + return ResolverFactory.createResolver({ + mainFields: resolveOptions.mainFields, + aliasFields: resolveOptions.aliasFields, + conditionNames: mergedConditionNames, + exportsFields: resolveOptions.exportsFields, + extensions: resolveOptions.extensions, + extensionAlias: resolveOptions.extensionAlias, + fileSystem: volume as any, // Cast to any to work around enhanced-resolve types + symlinks: false, + }); + }; + + const esmResolver = createResolver('esm'); + const cjsResolver = createResolver('commonjs'); + + return { + volume, + // Simple resolve function - most tests just need this + async resolve( + request: string, + context = '/app', + dependencyType: 'esm' | 'commonjs' = 'esm' + ): Promise { + const resolver = dependencyType === 'esm' ? esmResolver : cjsResolver; + try { + const result = await new Promise((resolve, reject) => { + resolver.resolve({}, context, request, {}, (err, result) => { + err ? reject(err) : resolve(result as string); + }); + }); + return result; + } catch { + return null; + } + }, + // Debug helper + listFiles(): string[] { + const files: string[] = []; + function walk(dir: string): void { + try { + const items = volume.readdirSync(dir) as string[]; + for (const item of items) { + const fullPath = `${dir}/${item}`; + const stat = volume.statSync(fullPath); + if (stat.isDirectory()) { + walk(fullPath); + } else { + files.push(fullPath); + } + } + } catch { + // Ignore errors + } + } + walk('/'); + return files; + }, + }; +} diff --git a/tests/resolver-cases/src/virtual-fs.ts b/tests/resolver-cases/src/virtual-fs.ts deleted file mode 100644 index 12de31165..000000000 --- a/tests/resolver-cases/src/virtual-fs.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Volume } from 'memfs'; -import type { IFs } from 'memfs'; - -export interface VirtualPackage { - name: string; - version: string; - packageJson: Record; - files: Record; -} - -export class VirtualFileSystem { - private volume: InstanceType; - public fs: IFs; - - constructor() { - this.volume = new Volume(); - this.fs = this.volume.promises as any; - } - - async createPackage(packagePath: string, pkg: VirtualPackage): Promise { - const basePath = packagePath.endsWith('/') - ? packagePath - : `${packagePath}/`; - - // Create package.json - const packageJsonPath = `${basePath}package.json`; - await this.volume.promises.mkdir(basePath, { recursive: true }); - await this.volume.promises.writeFile( - packageJsonPath, - JSON.stringify(pkg.packageJson, null, 2) - ); - - // Create all other files - for (const [filePath, content] of Object.entries(pkg.files)) { - const fullPath = `${basePath}${filePath}`; - const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/')); - - if (dirPath !== basePath.slice(0, -1)) { - await this.volume.promises.mkdir(dirPath, { recursive: true }); - } - - await this.volume.promises.writeFile(fullPath, content); - } - } - - getFileSystem(): InstanceType { - return this.volume; - } - - listFiles(): string[] { - const files: string[] = []; - const volume = this.volume; - - function walk(dir: string): void { - try { - const items = volume.readdirSync(dir) as string[]; - for (const item of items) { - const fullPath = `${dir}/${item}`; - const stat = volume.statSync(fullPath); - if (stat.isDirectory()) { - walk(fullPath); - } else { - files.push(fullPath); - } - } - } catch { - // Ignore errors - } - } - - walk('/'); - return files; - } -} From 631684aece82c3edce4a31120151b9a8ccae3016 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:21:21 +0200 Subject: [PATCH 04/21] chore: fixtures --- .../src/__fixtures__/asset-lib-simple.json | 13 ++ .../src/__fixtures__/asset-lib.json | 15 ++ .../src/__fixtures__/complex-lib.json | 34 ++++ .../src/__fixtures__/platform-lib.json | 17 ++ .../src/__fixtures__/react-strict-dom.json | 22 +++ .../src/__fixtures__/ts-platform-lib.json | 16 ++ .../src/__tests__/asset-resolution.test.ts | 42 +--- .../src/__tests__/exports-resolution.test.ts | 181 ++---------------- .../src/__tests__/platform-resolution.test.ts | 90 +-------- tests/resolver-cases/src/test-helpers.ts | 33 ++++ 10 files changed, 176 insertions(+), 287 deletions(-) create mode 100644 tests/resolver-cases/src/__fixtures__/asset-lib-simple.json create mode 100644 tests/resolver-cases/src/__fixtures__/asset-lib.json create mode 100644 tests/resolver-cases/src/__fixtures__/complex-lib.json create mode 100644 tests/resolver-cases/src/__fixtures__/platform-lib.json create mode 100644 tests/resolver-cases/src/__fixtures__/react-strict-dom.json create mode 100644 tests/resolver-cases/src/__fixtures__/ts-platform-lib.json diff --git a/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json b/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json new file mode 100644 index 000000000..115f6926e --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json @@ -0,0 +1,13 @@ +{ + "package.json": { + "name": "asset-lib", + "version": "1.0.0", + "main": "./index.js" + }, + "files": { + "index.js": "export const assets = require(\"./assets\");", + "assets/icon.png": "fake-png-content", + "assets/logo.jpg": "fake-jpg-content", + "assets/video.mp4": "fake-mp4-content" + } +} diff --git a/tests/resolver-cases/src/__fixtures__/asset-lib.json b/tests/resolver-cases/src/__fixtures__/asset-lib.json new file mode 100644 index 000000000..13a65c71d --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/asset-lib.json @@ -0,0 +1,15 @@ +{ + "package.json": { + "name": "asset-lib", + "version": "1.0.0", + "main": "./index.js" + }, + "files": { + "index.js": "export const assets = require(\"./assets\");", + "assets/icon.png": "fake-png-content", + "assets/icon@2x.png": "fake-png-content-2x", + "assets/icon@3x.png": "fake-png-content-3x", + "assets/logo.jpg": "fake-jpg-content", + "assets/video.mp4": "fake-mp4-content" + } +} diff --git a/tests/resolver-cases/src/__fixtures__/complex-lib.json b/tests/resolver-cases/src/__fixtures__/complex-lib.json new file mode 100644 index 000000000..36c12b05d --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/complex-lib.json @@ -0,0 +1,34 @@ +{ + "package.json": { + "name": "complex-lib", + "version": "2.1.0", + "exports": { + ".": { + "import": { + "react-native": "./esm/index.native.js", + "default": "./esm/index.js" + }, + "require": { + "react-native": "./cjs/index.native.js", + "default": "./cjs/index.js" + } + }, + "./utils": { + "import": "./esm/utils.js", + "require": "./cjs/utils.js" + }, + "./native-only": { + "react-native": "./native-specific.js" + } + } + }, + "files": { + "esm/index.js": "export const lib = 'esm-web';", + "esm/index.native.js": "export const lib = 'esm-native';", + "esm/utils.js": "export const utils = 'esm-utils';", + "cjs/index.js": "module.exports = { lib: 'cjs-web' };", + "cjs/index.native.js": "module.exports = { lib: 'cjs-native' };", + "cjs/utils.js": "module.exports = { utils: 'cjs-utils' };", + "native-specific.js": "export const feature = 'native-only';" + } +} diff --git a/tests/resolver-cases/src/__fixtures__/platform-lib.json b/tests/resolver-cases/src/__fixtures__/platform-lib.json new file mode 100644 index 000000000..34998493c --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/platform-lib.json @@ -0,0 +1,17 @@ +{ + "package.json": { + "name": "platform-specific-lib", + "version": "1.0.0", + "main": "./index" + }, + "files": { + "index.js": "export const platform = \"web\";", + "index.native.js": "export const platform = \"native\";", + "index.ios.js": "export const platform = \"ios\";", + "index.android.js": "export const platform = \"android\";", + "lib/utils.js": "export const utils = \"web\";", + "lib/utils.native.js": "export const utils = \"native\";", + "lib/utils.ios.js": "export const utils = \"ios\";", + "lib/utils.android.js": "export const utils = \"android\";" + } +} diff --git a/tests/resolver-cases/src/__fixtures__/react-strict-dom.json b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json new file mode 100644 index 000000000..2a5e3a693 --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json @@ -0,0 +1,22 @@ +{ + "package.json": { + "name": "react-strict-dom", + "version": "0.0.28", + "exports": { + ".": { + "react-native": "./dist/native/index.js", + "default": "./dist/dom/index.js" + }, + "./html": { + "react-native": "./dist/native/html.js", + "default": "./dist/dom/html.js" + } + } + }, + "files": { + "dist/native/index.js": "export * from './native-components';", + "dist/dom/index.js": "export * from './dom-components';", + "dist/native/html.js": "export const View = 'RCTView';", + "dist/dom/html.js": "export const View = 'div';" + } +} diff --git a/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json b/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json new file mode 100644 index 000000000..08df90ea5 --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json @@ -0,0 +1,16 @@ +{ + "package.json": { + "name": "typescript-platform-lib", + "version": "1.0.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "files": { + "dist/index.js": "export const platform = \"web\";", + "dist/index.d.ts": "export declare const platform: \"web\";", + "src/utils.ts": "export const utils = \"web\";", + "src/utils.native.ts": "export const utils = \"native\";", + "src/utils.ios.ts": "export const utils = \"ios\";", + "src/utils.android.ts": "export const utils = \"android\";" + } +} diff --git a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts index 150631725..e17b8a87c 100644 --- a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts @@ -1,22 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { setupTestEnvironment } from '../test-helpers.js'; +import { loadFixture, setupTestEnvironment } from '../test-helpers.js'; describe('Asset Resolution', () => { test('should resolve base asset when no scale specified and no scaled versions exist', async () => { const { resolve } = await setupTestEnvironment( - { - 'asset-lib': { - 'package.json': JSON.stringify({ - name: 'asset-lib', - version: '1.0.0', - main: './index.js', - }), - 'index.js': 'export const assets = require("./assets");', - 'assets/icon.png': 'fake-png-content', - 'assets/logo.jpg': 'fake-jpg-content', - 'assets/video.mp4': 'fake-mp4-content', - }, - }, + { 'asset-lib': loadFixture('asset-lib-simple') }, { platform: 'ios' } ); @@ -26,19 +14,7 @@ describe('Asset Resolution', () => { test('should resolve 2x scaled assets when available', async () => { const { resolve } = await setupTestEnvironment( - { - 'asset-lib': { - 'package.json': JSON.stringify({ - name: 'asset-lib', - version: '1.0.0', - main: './index.js', - }), - 'index.js': 'export const assets = require("./assets");', - 'assets/icon.png': 'fake-png-content', - 'assets/icon@2x.png': 'fake-png-content-2x', - 'assets/icon@3x.png': 'fake-png-content-3x', - }, - }, + { 'asset-lib': loadFixture('asset-lib') }, { platform: 'ios' } ); @@ -49,17 +25,7 @@ describe('Asset Resolution', () => { test('should resolve different asset formats', async () => { const { resolve } = await setupTestEnvironment( - { - 'asset-lib': { - 'package.json': JSON.stringify({ - name: 'asset-lib', - version: '1.0.0', - main: './index.js', - }), - 'assets/logo.jpg': 'fake-jpg-content', - 'assets/video.mp4': 'fake-mp4-content', - }, - }, + { 'asset-lib': loadFixture('asset-lib-simple') }, { platform: 'ios' } ); diff --git a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts index d82f93c79..482ee2dbd 100644 --- a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -1,41 +1,11 @@ import { describe, expect, test } from 'vitest'; -import { setupTestEnvironment } from '../test-helpers.js'; +import { loadFixture, setupTestEnvironment } from '../test-helpers.js'; describe('Package Exports Resolution', () => { describe('React Strict DOM pattern', () => { test('should resolve to native version for react-native condition', async () => { const { resolve } = await setupTestEnvironment( - { - 'react-strict-dom': { - 'package.json': JSON.stringify({ - name: 'react-strict-dom', - version: '0.0.36', - description: 'React Strict DOM', - exports: { - '.': { - 'react-native': { - types: './dist/native/index.d.ts', - default: './dist/native/index.js', - }, - default: { - types: './dist/dom/index.d.ts', - default: './dist/dom/index.js', - }, - }, - './babel-preset': './babel/preset.js', - './runtime': './dist/dom/runtime.js', - './package.json': './package.json', - }, - }), - 'dist/native/index.js': 'export const platform = "native";', - 'dist/native/index.d.ts': - 'export declare const platform: "native";', - 'dist/dom/index.js': 'export const platform = "dom";', - 'dist/dom/index.d.ts': 'export declare const platform: "dom";', - 'dist/dom/runtime.js': 'export const runtime = "dom";', - 'babel/preset.js': 'module.exports = {};', - }, - }, + { 'react-strict-dom': loadFixture('react-strict-dom') }, { platform: 'ios', enablePackageExports: true } ); @@ -47,98 +17,31 @@ describe('Package Exports Resolution', () => { test('should resolve to DOM version when package exports disabled', async () => { const { resolve } = await setupTestEnvironment( - { - 'react-strict-dom': { - 'package.json': JSON.stringify({ - name: 'react-strict-dom', - version: '0.0.36', - description: 'React Strict DOM', - exports: { - '.': { - 'react-native': { - types: './dist/native/index.d.ts', - default: './dist/native/index.js', - }, - default: { - types: './dist/dom/index.d.ts', - default: './dist/dom/index.js', - }, - }, - }, - }), - 'dist/native/index.js': 'export const platform = "native";', - 'dist/dom/index.js': 'export const platform = "dom";', - }, - }, + { 'react-strict-dom': loadFixture('react-strict-dom') }, { platform: 'ios', enablePackageExports: false } ); - // Without package exports, should fall back to main field resolution + // When exports are disabled, it should fallback to main field (which doesn't exist) + // and then resolve to index.js (which also doesn't exist in this case) const result = await resolve('react-strict-dom'); - expect(result).toBe(null); // No main field in this package + expect(result).toBe(null); }); test('should resolve subpath exports', async () => { const { resolve } = await setupTestEnvironment( - { - 'react-strict-dom': { - 'package.json': JSON.stringify({ - name: 'react-strict-dom', - version: '0.0.36', - exports: { - './babel-preset': './babel/preset.js', - './runtime': './dist/dom/runtime.js', - }, - }), - 'dist/dom/runtime.js': 'export const runtime = "dom";', - 'babel/preset.js': 'module.exports = {};', - }, - }, + { 'react-strict-dom': loadFixture('react-strict-dom') }, { platform: 'ios', enablePackageExports: true } ); - const runtimeResult = await resolve('react-strict-dom/runtime'); - expect(runtimeResult).toBe( - '/node_modules/react-strict-dom/dist/dom/runtime.js' - ); - - const babelResult = await resolve('react-strict-dom/babel-preset'); - expect(babelResult).toBe( - '/node_modules/react-strict-dom/babel/preset.js' - ); + const result = await resolve('react-strict-dom/html'); + expect(result).toBe('/node_modules/react-strict-dom/dist/native/html.js'); }); }); describe('Complex exports patterns', () => { test('should resolve ESM imports with react-native condition', async () => { const { resolve } = await setupTestEnvironment( - { - 'complex-lib': { - 'package.json': JSON.stringify({ - name: 'complex-exports-lib', - version: '1.0.0', - exports: { - '.': { - import: { - 'react-native': './esm/index.native.js', - default: './esm/index.js', - }, - require: { - 'react-native': './cjs/index.native.js', - default: './cjs/index.js', - }, - }, - }, - }), - 'esm/index.js': - 'export const format = "esm"; export const platform = "web";', - 'esm/index.native.js': - 'export const format = "esm"; export const platform = "native";', - 'cjs/index.js': 'exports.format = "cjs"; exports.platform = "web";', - 'cjs/index.native.js': - 'exports.format = "cjs"; exports.platform = "native";', - }, - }, + { 'complex-lib': loadFixture('complex-lib') }, { platform: 'ios', enablePackageExports: true } ); @@ -148,33 +51,7 @@ describe('Package Exports Resolution', () => { test('should resolve CommonJS requires with react-native condition', async () => { const { resolve } = await setupTestEnvironment( - { - 'complex-lib': { - 'package.json': JSON.stringify({ - name: 'complex-exports-lib', - version: '1.0.0', - exports: { - '.': { - import: { - 'react-native': './esm/index.native.js', - default: './esm/index.js', - }, - require: { - 'react-native': './cjs/index.native.js', - default: './cjs/index.js', - }, - }, - }, - }), - 'esm/index.js': - 'export const format = "esm"; export const platform = "web";', - 'esm/index.native.js': - 'export const format = "esm"; export const platform = "native";', - 'cjs/index.js': 'exports.format = "cjs"; exports.platform = "web";', - 'cjs/index.native.js': - 'exports.format = "cjs"; exports.platform = "native";', - }, - }, + { 'complex-lib': loadFixture('complex-lib') }, { platform: 'ios', enablePackageExports: true } ); @@ -184,22 +61,7 @@ describe('Package Exports Resolution', () => { test('should resolve utils subpath', async () => { const { resolve } = await setupTestEnvironment( - { - 'complex-lib': { - 'package.json': JSON.stringify({ - name: 'complex-exports-lib', - version: '1.0.0', - exports: { - './utils': { - import: './esm/utils.js', - require: './cjs/utils.js', - }, - }, - }), - 'esm/utils.js': 'export const utils = "esm";', - 'cjs/utils.js': 'exports.utils = "cjs";', - }, - }, + { 'complex-lib': loadFixture('complex-lib') }, { platform: 'ios', enablePackageExports: true } ); @@ -212,25 +74,12 @@ describe('Package Exports Resolution', () => { test('should resolve react-native only exports', async () => { const { resolve } = await setupTestEnvironment( - { - 'complex-lib': { - 'package.json': JSON.stringify({ - name: 'complex-exports-lib', - version: '1.0.0', - exports: { - './native': { - 'react-native': './native/index.js', - }, - }, - }), - 'native/index.js': 'export const platform = "native-only";', - }, - }, + { 'complex-lib': loadFixture('complex-lib') }, { platform: 'ios', enablePackageExports: true } ); - const result = await resolve('complex-lib/native'); - expect(result).toBe('/node_modules/complex-lib/native/index.js'); + const result = await resolve('complex-lib/native-only'); + expect(result).toBe('/node_modules/complex-lib/native-specific.js'); }); }); }); diff --git a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts index 7061198b3..0b0983d3d 100644 --- a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -1,22 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { setupTestEnvironment } from '../test-helpers.js'; +import { loadFixtures, setupTestEnvironment } from '../test-helpers.js'; describe('Platform Resolution', () => { test('should resolve iOS platform files when platform is ios', async () => { const { resolve } = await setupTestEnvironment( - { - 'platform-lib': { - 'package.json': JSON.stringify({ - name: 'platform-specific-lib', - version: '1.0.0', - main: './index', - }), - 'index.js': 'export const platform = "web";', - 'index.native.js': 'export const platform = "native";', - 'index.ios.js': 'export const platform = "ios";', - 'index.android.js': 'export const platform = "android";', - }, - }, + loadFixtures({ 'platform-lib': 'platform-lib' }), { platform: 'ios', preferNativePlatform: true } ); @@ -26,19 +14,7 @@ describe('Platform Resolution', () => { test('should resolve Android platform files when platform is android', async () => { const { resolve } = await setupTestEnvironment( - { - 'platform-lib': { - 'package.json': JSON.stringify({ - name: 'platform-specific-lib', - version: '1.0.0', - main: './index', - }), - 'index.js': 'export const platform = "web";', - 'index.native.js': 'export const platform = "native";', - 'index.ios.js': 'export const platform = "ios";', - 'index.android.js': 'export const platform = "android";', - }, - }, + loadFixtures({ 'platform-lib': 'platform-lib' }), { platform: 'android', preferNativePlatform: true } ); @@ -48,19 +24,7 @@ describe('Platform Resolution', () => { test('should fallback to native when platform file not found', async () => { const { resolve } = await setupTestEnvironment( - { - 'platform-lib': { - 'package.json': JSON.stringify({ - name: 'platform-specific-lib', - version: '1.0.0', - main: './index', - }), - 'index.js': 'export const platform = "web";', - 'index.native.js': 'export const platform = "native";', - 'index.ios.js': 'export const platform = "ios";', - 'index.android.js': 'export const platform = "android";', - }, - }, + loadFixtures({ 'platform-lib': 'platform-lib' }), { platform: 'web', preferNativePlatform: true } ); @@ -70,22 +34,7 @@ describe('Platform Resolution', () => { test('should resolve platform-specific TypeScript files', async () => { const { resolve } = await setupTestEnvironment( - { - 'ts-platform-lib': { - 'package.json': JSON.stringify({ - name: 'typescript-platform-lib', - version: '1.0.0', - main: './dist/index.js', - types: './dist/index.d.ts', - }), - 'dist/index.js': 'export const platform = "web";', - 'dist/index.d.ts': 'export declare const platform: "web";', - 'src/utils.ts': 'export const utils = "web";', - 'src/utils.native.ts': 'export const utils = "native";', - 'src/utils.ios.ts': 'export const utils = "ios";', - 'src/utils.android.ts': 'export const utils = "android";', - }, - }, + loadFixtures({ 'ts-platform-lib': 'ts-platform-lib' }), { platform: 'ios', preferNativePlatform: true } ); @@ -95,19 +44,7 @@ describe('Platform Resolution', () => { test('should prefer platform over native when preferNativePlatform is false', async () => { const { resolve } = await setupTestEnvironment( - { - 'platform-lib': { - 'package.json': JSON.stringify({ - name: 'platform-specific-lib', - version: '1.0.0', - main: './index', - }), - 'index.js': 'export const platform = "web";', - 'index.native.js': 'export const platform = "native";', - 'index.ios.js': 'export const platform = "ios";', - 'index.android.js': 'export const platform = "android";', - }, - }, + loadFixtures({ 'platform-lib': 'platform-lib' }), { platform: 'ios', preferNativePlatform: false } ); @@ -117,20 +54,7 @@ describe('Platform Resolution', () => { test('should resolve nested platform-specific files', async () => { const { resolve } = await setupTestEnvironment( - { - 'platform-lib': { - 'package.json': JSON.stringify({ - name: 'platform-specific-lib', - version: '1.0.0', - main: './index', - }), - 'index.js': 'export const platform = "web";', - 'lib/utils.js': 'export const utils = "web";', - 'lib/utils.native.js': 'export const utils = "native";', - 'lib/utils.ios.js': 'export const utils = "ios";', - 'lib/utils.android.js': 'export const utils = "android";', - }, - }, + loadFixtures({ 'platform-lib': 'platform-lib' }), { platform: 'android', preferNativePlatform: true } ); diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 545bda6ff..9d006d6f3 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { type ResolveOptions, getResolveOptions } from '@callstack/repack'; import { ResolverFactory } from 'enhanced-resolve'; import { Volume } from 'memfs'; @@ -6,6 +8,37 @@ export interface TestOptions extends ResolveOptions { platform?: string; } +interface FixtureData { + 'package.json': Record; + files: Record; +} + +// Load fixture data from JSON files +export function loadFixture(fixtureName: string): Record { + const fixturePath = join(__dirname, '__fixtures__', `${fixtureName}.json`); + const fixtureData: FixtureData = JSON.parse( + readFileSync(fixturePath, 'utf8') + ); + + return { + 'package.json': JSON.stringify(fixtureData['package.json']), + ...fixtureData.files, + }; +} + +// Load multiple fixtures +export function loadFixtures( + fixtures: Record +): Record> { + const result: Record> = {}; + + for (const [packageName, fixtureName] of Object.entries(fixtures)) { + result[packageName] = loadFixture(fixtureName); + } + + return result; +} + // Simple function to create a package in the virtual filesystem async function createPackage( volume: InstanceType, From 7ffe39e0de29001afd49e676e9a6d9705c6ddc41 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:37:43 +0200 Subject: [PATCH 05/21] fix: reconstruct webpack/rspack behaviour for condition names --- tests/resolver-cases/src/test-helpers.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 9d006d6f3..388432650 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -88,16 +88,13 @@ export async function setupTestEnvironment( // Create resolvers for both ESM and CommonJS const createResolver = (dependencyType: 'esm' | 'commonjs') => { - const dependencyOptions = resolveOptions.byDependency[dependencyType] || {}; - const mergedConditionNames = [ - ...resolveOptions.conditionNames, - ...dependencyOptions.conditionNames, - ]; + const specificConditionNames = + resolveOptions.byDependency[dependencyType].conditionNames; return ResolverFactory.createResolver({ mainFields: resolveOptions.mainFields, aliasFields: resolveOptions.aliasFields, - conditionNames: mergedConditionNames, + conditionNames: specificConditionNames ?? resolveOptions.conditionNames, exportsFields: resolveOptions.exportsFields, extensions: resolveOptions.extensions, extensionAlias: resolveOptions.extensionAlias, From babc1b03e7bae2d79b9f6b650fd41f871b29d954 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:41:17 +0200 Subject: [PATCH 06/21] chore: align strict dom tests --- .../src/__fixtures__/react-strict-dom.json | 17 +++++++++++------ .../src/__tests__/exports-resolution.test.ts | 10 ---------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/resolver-cases/src/__fixtures__/react-strict-dom.json b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json index 2a5e3a693..904a7ac71 100644 --- a/tests/resolver-cases/src/__fixtures__/react-strict-dom.json +++ b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json @@ -4,13 +4,18 @@ "version": "0.0.28", "exports": { ".": { - "react-native": "./dist/native/index.js", - "default": "./dist/dom/index.js" + "react-native": { + "types": "./dist/native/index.d.ts", + "default": "./dist/native/index.js" + }, + "default": { + "types": "./dist/dom/index.d.ts", + "default": "./dist/dom/index.js" + } }, - "./html": { - "react-native": "./dist/native/html.js", - "default": "./dist/dom/html.js" - } + "./babel-preset": "./babel/preset.js", + "./runtime": "./dist/dom/runtime.js", + "./package.json": "./package.json" } }, "files": { diff --git a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts index 482ee2dbd..68e43e2e3 100644 --- a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -26,16 +26,6 @@ describe('Package Exports Resolution', () => { const result = await resolve('react-strict-dom'); expect(result).toBe(null); }); - - test('should resolve subpath exports', async () => { - const { resolve } = await setupTestEnvironment( - { 'react-strict-dom': loadFixture('react-strict-dom') }, - { platform: 'ios', enablePackageExports: true } - ); - - const result = await resolve('react-strict-dom/html'); - expect(result).toBe('/node_modules/react-strict-dom/dist/native/html.js'); - }); }); describe('Complex exports patterns', () => { From b6cbf52db325a89c2dda45289ca2bf553a060406 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:41:33 +0200 Subject: [PATCH 07/21] chore: rename --- tests/resolver-cases/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resolver-cases/package.json b/tests/resolver-cases/package.json index 4a2757388..e48baa651 100644 --- a/tests/resolver-cases/package.json +++ b/tests/resolver-cases/package.json @@ -1,5 +1,5 @@ { - "name": "@repack/resolver-cases", + "name": "resolver-cases-test", "version": "0.0.1", "private": true, "type": "module", From 4db37c24e1eb7c9e24594c2217ec9a3b086c9d39 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:46:35 +0200 Subject: [PATCH 08/21] refactor: use array for files in fixtures --- .../src/__fixtures__/asset-lib-simple.json | 12 +++++------ .../src/__fixtures__/asset-lib.json | 16 +++++++-------- .../src/__fixtures__/complex-lib.json | 18 ++++++++--------- .../src/__fixtures__/platform-lib.json | 20 +++++++++---------- .../src/__fixtures__/react-strict-dom.json | 14 +++++++------ .../src/__fixtures__/ts-platform-lib.json | 16 +++++++-------- tests/resolver-cases/src/test-helpers.ts | 12 ++++++++--- 7 files changed, 58 insertions(+), 50 deletions(-) diff --git a/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json b/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json index 115f6926e..b17b52089 100644 --- a/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json +++ b/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json @@ -4,10 +4,10 @@ "version": "1.0.0", "main": "./index.js" }, - "files": { - "index.js": "export const assets = require(\"./assets\");", - "assets/icon.png": "fake-png-content", - "assets/logo.jpg": "fake-jpg-content", - "assets/video.mp4": "fake-mp4-content" - } + "files": [ + "index.js", + "assets/icon.png", + "assets/logo.jpg", + "assets/video.mp4" + ] } diff --git a/tests/resolver-cases/src/__fixtures__/asset-lib.json b/tests/resolver-cases/src/__fixtures__/asset-lib.json index 13a65c71d..eb520d3d7 100644 --- a/tests/resolver-cases/src/__fixtures__/asset-lib.json +++ b/tests/resolver-cases/src/__fixtures__/asset-lib.json @@ -4,12 +4,12 @@ "version": "1.0.0", "main": "./index.js" }, - "files": { - "index.js": "export const assets = require(\"./assets\");", - "assets/icon.png": "fake-png-content", - "assets/icon@2x.png": "fake-png-content-2x", - "assets/icon@3x.png": "fake-png-content-3x", - "assets/logo.jpg": "fake-jpg-content", - "assets/video.mp4": "fake-mp4-content" - } + "files": [ + "index.js", + "assets/icon.png", + "assets/icon@2x.png", + "assets/icon@3x.png", + "assets/logo.jpg", + "assets/video.mp4" + ] } diff --git a/tests/resolver-cases/src/__fixtures__/complex-lib.json b/tests/resolver-cases/src/__fixtures__/complex-lib.json index 36c12b05d..650572bd7 100644 --- a/tests/resolver-cases/src/__fixtures__/complex-lib.json +++ b/tests/resolver-cases/src/__fixtures__/complex-lib.json @@ -22,13 +22,13 @@ } } }, - "files": { - "esm/index.js": "export const lib = 'esm-web';", - "esm/index.native.js": "export const lib = 'esm-native';", - "esm/utils.js": "export const utils = 'esm-utils';", - "cjs/index.js": "module.exports = { lib: 'cjs-web' };", - "cjs/index.native.js": "module.exports = { lib: 'cjs-native' };", - "cjs/utils.js": "module.exports = { utils: 'cjs-utils' };", - "native-specific.js": "export const feature = 'native-only';" - } + "files": [ + "esm/index.js", + "esm/index.native.js", + "esm/utils.js", + "cjs/index.js", + "cjs/index.native.js", + "cjs/utils.js", + "native-specific.js" + ] } diff --git a/tests/resolver-cases/src/__fixtures__/platform-lib.json b/tests/resolver-cases/src/__fixtures__/platform-lib.json index 34998493c..c30355b58 100644 --- a/tests/resolver-cases/src/__fixtures__/platform-lib.json +++ b/tests/resolver-cases/src/__fixtures__/platform-lib.json @@ -4,14 +4,14 @@ "version": "1.0.0", "main": "./index" }, - "files": { - "index.js": "export const platform = \"web\";", - "index.native.js": "export const platform = \"native\";", - "index.ios.js": "export const platform = \"ios\";", - "index.android.js": "export const platform = \"android\";", - "lib/utils.js": "export const utils = \"web\";", - "lib/utils.native.js": "export const utils = \"native\";", - "lib/utils.ios.js": "export const utils = \"ios\";", - "lib/utils.android.js": "export const utils = \"android\";" - } + "files": [ + "index.js", + "index.native.js", + "index.ios.js", + "index.android.js", + "lib/utils.js", + "lib/utils.native.js", + "lib/utils.ios.js", + "lib/utils.android.js" + ] } diff --git a/tests/resolver-cases/src/__fixtures__/react-strict-dom.json b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json index 904a7ac71..a85a48fa6 100644 --- a/tests/resolver-cases/src/__fixtures__/react-strict-dom.json +++ b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json @@ -18,10 +18,12 @@ "./package.json": "./package.json" } }, - "files": { - "dist/native/index.js": "export * from './native-components';", - "dist/dom/index.js": "export * from './dom-components';", - "dist/native/html.js": "export const View = 'RCTView';", - "dist/dom/html.js": "export const View = 'div';" - } + "files": [ + "dist/native/index.js", + "dist/native/index.d.ts", + "dist/dom/index.js", + "dist/dom/index.d.ts", + "dist/dom/runtime.js", + "babel/preset.js" + ] } diff --git a/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json b/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json index 08df90ea5..2f0741532 100644 --- a/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json +++ b/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json @@ -5,12 +5,12 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts" }, - "files": { - "dist/index.js": "export const platform = \"web\";", - "dist/index.d.ts": "export declare const platform: \"web\";", - "src/utils.ts": "export const utils = \"web\";", - "src/utils.native.ts": "export const utils = \"native\";", - "src/utils.ios.ts": "export const utils = \"ios\";", - "src/utils.android.ts": "export const utils = \"android\";" - } + "files": [ + "dist/index.js", + "dist/index.d.ts", + "src/utils.ts", + "src/utils.native.ts", + "src/utils.ios.ts", + "src/utils.android.ts" + ] } diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 388432650..0f5325c0b 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -10,7 +10,7 @@ export interface TestOptions extends ResolveOptions { interface FixtureData { 'package.json': Record; - files: Record; + files: string[]; } // Load fixture data from JSON files @@ -20,10 +20,16 @@ export function loadFixture(fixtureName: string): Record { readFileSync(fixturePath, 'utf8') ); - return { + const result: Record = { 'package.json': JSON.stringify(fixtureData['package.json']), - ...fixtureData.files, }; + + // Create empty files for each path in the files array + for (const filePath of fixtureData.files) { + result[filePath] = `// ${filePath}`; + } + + return result; } // Load multiple fixtures From 87cadf2c31dff3a5e6f7a4f7a3759f4d899a90c3 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Fri, 27 Jun 2025 00:49:05 +0200 Subject: [PATCH 09/21] chore: remove loadFixtures --- .../src/__tests__/platform-resolution.test.ts | 14 +++++++------- tests/resolver-cases/src/test-helpers.ts | 13 ------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts index 0b0983d3d..1e4d969d5 100644 --- a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from 'vitest'; -import { loadFixtures, setupTestEnvironment } from '../test-helpers.js'; +import { loadFixture, setupTestEnvironment } from '../test-helpers.js'; describe('Platform Resolution', () => { test('should resolve iOS platform files when platform is ios', async () => { const { resolve } = await setupTestEnvironment( - loadFixtures({ 'platform-lib': 'platform-lib' }), + { 'platform-lib': loadFixture('platform-lib') }, { platform: 'ios', preferNativePlatform: true } ); @@ -14,7 +14,7 @@ describe('Platform Resolution', () => { test('should resolve Android platform files when platform is android', async () => { const { resolve } = await setupTestEnvironment( - loadFixtures({ 'platform-lib': 'platform-lib' }), + { 'platform-lib': loadFixture('platform-lib') }, { platform: 'android', preferNativePlatform: true } ); @@ -24,7 +24,7 @@ describe('Platform Resolution', () => { test('should fallback to native when platform file not found', async () => { const { resolve } = await setupTestEnvironment( - loadFixtures({ 'platform-lib': 'platform-lib' }), + { 'platform-lib': loadFixture('platform-lib') }, { platform: 'web', preferNativePlatform: true } ); @@ -34,7 +34,7 @@ describe('Platform Resolution', () => { test('should resolve platform-specific TypeScript files', async () => { const { resolve } = await setupTestEnvironment( - loadFixtures({ 'ts-platform-lib': 'ts-platform-lib' }), + { 'ts-platform-lib': loadFixture('ts-platform-lib') }, { platform: 'ios', preferNativePlatform: true } ); @@ -44,7 +44,7 @@ describe('Platform Resolution', () => { test('should prefer platform over native when preferNativePlatform is false', async () => { const { resolve } = await setupTestEnvironment( - loadFixtures({ 'platform-lib': 'platform-lib' }), + { 'platform-lib': loadFixture('platform-lib') }, { platform: 'ios', preferNativePlatform: false } ); @@ -54,7 +54,7 @@ describe('Platform Resolution', () => { test('should resolve nested platform-specific files', async () => { const { resolve } = await setupTestEnvironment( - loadFixtures({ 'platform-lib': 'platform-lib' }), + { 'platform-lib': loadFixture('platform-lib') }, { platform: 'android', preferNativePlatform: true } ); diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 0f5325c0b..e2b370c04 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -32,19 +32,6 @@ export function loadFixture(fixtureName: string): Record { return result; } -// Load multiple fixtures -export function loadFixtures( - fixtures: Record -): Record> { - const result: Record> = {}; - - for (const [packageName, fixtureName] of Object.entries(fixtures)) { - result[packageName] = loadFixture(fixtureName); - } - - return result; -} - // Simple function to create a package in the virtual filesystem async function createPackage( volume: InstanceType, From 8257e5642568779f4df0022856d44ea7815c1246 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 14:33:27 +0200 Subject: [PATCH 10/21] refactor: simplify --- tests/resolver-cases/src/test-helpers.ts | 156 +++++++++++++---------- 1 file changed, 90 insertions(+), 66 deletions(-) diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index e2b370c04..67d88d670 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -1,13 +1,9 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { type ResolveOptions, getResolveOptions } from '@callstack/repack'; +import fs from 'node:fs'; +import path from 'node:path'; +import { getResolveOptions } from '@callstack/repack'; import { ResolverFactory } from 'enhanced-resolve'; import { Volume } from 'memfs'; -export interface TestOptions extends ResolveOptions { - platform?: string; -} - interface FixtureData { 'package.json': Record; files: string[]; @@ -15,9 +11,11 @@ interface FixtureData { // Load fixture data from JSON files export function loadFixture(fixtureName: string): Record { - const fixturePath = join(__dirname, '__fixtures__', `${fixtureName}.json`); + const fixtureDir = path.join(__dirname, '__fixtures__'); + const fixturePath = path.join(fixtureDir, `${fixtureName}.json`); + const fixtureData: FixtureData = JSON.parse( - readFileSync(fixturePath, 'utf8') + fs.readFileSync(fixturePath, 'utf8') ); const result: Record = { @@ -57,22 +55,61 @@ async function createPackage( } } -// Main setup function - creates filesystem and resolver -export async function setupTestEnvironment( - packages: Record>, - options: TestOptions = {} -) { - const volume = new Volume(); - const platform = options.platform || 'ios'; - - // Ensure node_modules directory exists first - await volume.promises.mkdir('/node_modules', { recursive: true }); +// Helper function to resolve modules using the configured resolvers +function createResolveFunction(esmResolver: any, cjsResolver: any) { + return async function resolve( + request: string, + context = '/app', + dependencyType: 'esm' | 'commonjs' = 'esm' + ): Promise { + const resolver = dependencyType === 'esm' ? esmResolver : cjsResolver; + try { + const result = await new Promise((resolve, reject) => { + resolver.resolve({}, context, request, {}, (err: any, result: any) => { + err ? reject(err) : resolve(result as string); + }); + }); + return result; + } catch { + return null; + } + }; +} - // Create all packages in node_modules - for (const [packageName, files] of Object.entries(packages)) { - await createPackage(volume, `/node_modules/${packageName}`, files); - } +// Helper function to list all files in the virtual filesystem +function createListFilesFunction(volume: InstanceType) { + return function listFiles(): string[] { + const files: string[] = []; + function walk(dir: string): void { + try { + const items = volume.readdirSync(dir) as string[]; + for (const item of items) { + const fullPath = `${dir}/${item}`; + const stat = volume.statSync(fullPath); + if (stat.isDirectory()) { + walk(fullPath); + } else { + files.push(fullPath); + } + } + } catch { + // Ignore errors + } + } + walk('/'); + return files; + }; +} +// Helper function to create resolvers with Repack options +function createResolvers( + volume: InstanceType, + platform: string, + options: { + enablePackageExports?: boolean; + preferNativePlatform?: boolean; + } = {} +) { // Get resolve options from Repack const resolveOptions = getResolveOptions(platform, { enablePackageExports: options.enablePackageExports, @@ -96,50 +133,37 @@ export async function setupTestEnvironment( }); }; - const esmResolver = createResolver('esm'); - const cjsResolver = createResolver('commonjs'); + return { + esm: createResolver('esm'), + cjs: createResolver('commonjs'), + }; +} + +// Main setup function - creates filesystem and resolver +export async function setupTestEnvironment( + packages: Record>, + options: { + platform?: string; + enablePackageExports?: boolean; + preferNativePlatform?: boolean; + } = {} +) { + const volume = new Volume(); + const platform = options.platform || 'ios'; + + // Ensure node_modules directory exists first + await volume.promises.mkdir('/node_modules', { recursive: true }); + + // Create all packages in node_modules + for (const [packageName, files] of Object.entries(packages)) { + await createPackage(volume, `/node_modules/${packageName}`, files); + } + + // Create resolvers using the helper function + const resolvers = createResolvers(volume, platform, options); return { - volume, - // Simple resolve function - most tests just need this - async resolve( - request: string, - context = '/app', - dependencyType: 'esm' | 'commonjs' = 'esm' - ): Promise { - const resolver = dependencyType === 'esm' ? esmResolver : cjsResolver; - try { - const result = await new Promise((resolve, reject) => { - resolver.resolve({}, context, request, {}, (err, result) => { - err ? reject(err) : resolve(result as string); - }); - }); - return result; - } catch { - return null; - } - }, - // Debug helper - listFiles(): string[] { - const files: string[] = []; - function walk(dir: string): void { - try { - const items = volume.readdirSync(dir) as string[]; - for (const item of items) { - const fullPath = `${dir}/${item}`; - const stat = volume.statSync(fullPath); - if (stat.isDirectory()) { - walk(fullPath); - } else { - files.push(fullPath); - } - } - } catch { - // Ignore errors - } - } - walk('/'); - return files; - }, + resolve: createResolveFunction(resolvers.esm, resolvers.cjs), + listFiles: createListFilesFunction(volume), }; } From 4b50c0671a4316c586ddcca340a28803d2085103 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 14:40:40 +0200 Subject: [PATCH 11/21] refactor: cleanup setup --- .../src/__tests__/asset-resolution.test.ts | 23 +++--- .../src/__tests__/exports-resolution.test.ts | 50 ++++++------ .../src/__tests__/platform-resolution.test.ts | 78 ++++++++++--------- tests/resolver-cases/src/test-helpers.ts | 23 ++++-- 4 files changed, 91 insertions(+), 83 deletions(-) diff --git a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts index e17b8a87c..8e30c0423 100644 --- a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts @@ -1,22 +1,20 @@ import { describe, expect, test } from 'vitest'; -import { loadFixture, setupTestEnvironment } from '../test-helpers.js'; +import { setupTestEnvironment } from '../test-helpers.js'; describe('Asset Resolution', () => { test('should resolve base asset when no scale specified and no scaled versions exist', async () => { - const { resolve } = await setupTestEnvironment( - { 'asset-lib': loadFixture('asset-lib-simple') }, - { platform: 'ios' } - ); + const { resolve } = await setupTestEnvironment(['asset-lib-simple'], { + platform: 'ios', + }); const result = await resolve('asset-lib/assets/icon.png'); expect(result).toBe('/node_modules/asset-lib/assets/icon.png'); }); test('should resolve 2x scaled assets when available', async () => { - const { resolve } = await setupTestEnvironment( - { 'asset-lib': loadFixture('asset-lib') }, - { platform: 'ios' } - ); + const { resolve } = await setupTestEnvironment(['asset-lib'], { + platform: 'ios', + }); // React Native prefers scaled assets when available const result = await resolve('asset-lib/assets/icon.png'); @@ -24,10 +22,9 @@ describe('Asset Resolution', () => { }); test('should resolve different asset formats', async () => { - const { resolve } = await setupTestEnvironment( - { 'asset-lib': loadFixture('asset-lib-simple') }, - { platform: 'ios' } - ); + const { resolve } = await setupTestEnvironment(['asset-lib-simple'], { + platform: 'ios', + }); const jpgResult = await resolve('asset-lib/assets/logo.jpg'); expect(jpgResult).toBe('/node_modules/asset-lib/assets/logo.jpg'); diff --git a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts index 68e43e2e3..5051c9423 100644 --- a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -1,13 +1,13 @@ import { describe, expect, test } from 'vitest'; -import { loadFixture, setupTestEnvironment } from '../test-helpers.js'; +import { setupTestEnvironment } from '../test-helpers.js'; describe('Package Exports Resolution', () => { describe('React Strict DOM pattern', () => { test('should resolve to native version for react-native condition', async () => { - const { resolve } = await setupTestEnvironment( - { 'react-strict-dom': loadFixture('react-strict-dom') }, - { platform: 'ios', enablePackageExports: true } - ); + const { resolve } = await setupTestEnvironment(['react-strict-dom'], { + platform: 'ios', + enablePackageExports: true, + }); const result = await resolve('react-strict-dom'); expect(result).toBe( @@ -16,10 +16,10 @@ describe('Package Exports Resolution', () => { }); test('should resolve to DOM version when package exports disabled', async () => { - const { resolve } = await setupTestEnvironment( - { 'react-strict-dom': loadFixture('react-strict-dom') }, - { platform: 'ios', enablePackageExports: false } - ); + const { resolve } = await setupTestEnvironment(['react-strict-dom'], { + platform: 'ios', + enablePackageExports: false, + }); // When exports are disabled, it should fallback to main field (which doesn't exist) // and then resolve to index.js (which also doesn't exist in this case) @@ -30,30 +30,30 @@ describe('Package Exports Resolution', () => { describe('Complex exports patterns', () => { test('should resolve ESM imports with react-native condition', async () => { - const { resolve } = await setupTestEnvironment( - { 'complex-lib': loadFixture('complex-lib') }, - { platform: 'ios', enablePackageExports: true } - ); + const { resolve } = await setupTestEnvironment(['complex-lib'], { + platform: 'ios', + enablePackageExports: true, + }); const result = await resolve('complex-lib', '/app', 'esm'); expect(result).toBe('/node_modules/complex-lib/esm/index.native.js'); }); test('should resolve CommonJS requires with react-native condition', async () => { - const { resolve } = await setupTestEnvironment( - { 'complex-lib': loadFixture('complex-lib') }, - { platform: 'ios', enablePackageExports: true } - ); + const { resolve } = await setupTestEnvironment(['complex-lib'], { + platform: 'ios', + enablePackageExports: true, + }); const result = await resolve('complex-lib', '/app', 'commonjs'); expect(result).toBe('/node_modules/complex-lib/cjs/index.native.js'); }); test('should resolve utils subpath', async () => { - const { resolve } = await setupTestEnvironment( - { 'complex-lib': loadFixture('complex-lib') }, - { platform: 'ios', enablePackageExports: true } - ); + const { resolve } = await setupTestEnvironment(['complex-lib'], { + platform: 'ios', + enablePackageExports: true, + }); const esmResult = await resolve('complex-lib/utils', '/app', 'esm'); expect(esmResult).toBe('/node_modules/complex-lib/esm/utils.js'); @@ -63,10 +63,10 @@ describe('Package Exports Resolution', () => { }); test('should resolve react-native only exports', async () => { - const { resolve } = await setupTestEnvironment( - { 'complex-lib': loadFixture('complex-lib') }, - { platform: 'ios', enablePackageExports: true } - ); + const { resolve } = await setupTestEnvironment(['complex-lib'], { + platform: 'ios', + enablePackageExports: true, + }); const result = await resolve('complex-lib/native-only'); expect(result).toBe('/node_modules/complex-lib/native-specific.js'); diff --git a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts index 1e4d969d5..391d4787e 100644 --- a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -1,64 +1,68 @@ import { describe, expect, test } from 'vitest'; -import { loadFixture, setupTestEnvironment } from '../test-helpers.js'; +import { setupTestEnvironment } from '../test-helpers.js'; describe('Platform Resolution', () => { test('should resolve iOS platform files when platform is ios', async () => { - const { resolve } = await setupTestEnvironment( - { 'platform-lib': loadFixture('platform-lib') }, - { platform: 'ios', preferNativePlatform: true } - ); + const { resolve } = await setupTestEnvironment(['platform-lib'], { + platform: 'ios', + preferNativePlatform: true, + }); - const result = await resolve('platform-lib'); - expect(result).toBe('/node_modules/platform-lib/index.ios.js'); + const result = await resolve('platform-specific-lib'); + expect(result).toBe('/node_modules/platform-specific-lib/index.ios.js'); }); test('should resolve Android platform files when platform is android', async () => { - const { resolve } = await setupTestEnvironment( - { 'platform-lib': loadFixture('platform-lib') }, - { platform: 'android', preferNativePlatform: true } - ); + const { resolve } = await setupTestEnvironment(['platform-lib'], { + platform: 'android', + preferNativePlatform: true, + }); - const result = await resolve('platform-lib'); - expect(result).toBe('/node_modules/platform-lib/index.android.js'); + const result = await resolve('platform-specific-lib'); + expect(result).toBe('/node_modules/platform-specific-lib/index.android.js'); }); test('should fallback to native when platform file not found', async () => { - const { resolve } = await setupTestEnvironment( - { 'platform-lib': loadFixture('platform-lib') }, - { platform: 'web', preferNativePlatform: true } - ); + const { resolve } = await setupTestEnvironment(['platform-lib'], { + platform: 'web', + preferNativePlatform: true, + }); - const result = await resolve('platform-lib'); - expect(result).toBe('/node_modules/platform-lib/index.native.js'); + const result = await resolve('platform-specific-lib'); + expect(result).toBe('/node_modules/platform-specific-lib/index.native.js'); }); test('should resolve platform-specific TypeScript files', async () => { - const { resolve } = await setupTestEnvironment( - { 'ts-platform-lib': loadFixture('ts-platform-lib') }, - { platform: 'ios', preferNativePlatform: true } - ); + const { resolve } = await setupTestEnvironment(['ts-platform-lib'], { + platform: 'ios', + preferNativePlatform: true, + }); - const result = await resolve('ts-platform-lib/src/utils'); - expect(result).toBe('/node_modules/ts-platform-lib/src/utils.ios.ts'); + const result = await resolve('typescript-platform-lib/src/utils'); + expect(result).toBe( + '/node_modules/typescript-platform-lib/src/utils.ios.ts' + ); }); test('should prefer platform over native when preferNativePlatform is false', async () => { - const { resolve } = await setupTestEnvironment( - { 'platform-lib': loadFixture('platform-lib') }, - { platform: 'ios', preferNativePlatform: false } - ); + const { resolve } = await setupTestEnvironment(['platform-lib'], { + platform: 'ios', + preferNativePlatform: false, + }); - const result = await resolve('platform-lib'); - expect(result).toBe('/node_modules/platform-lib/index.ios.js'); + const result = await resolve('platform-specific-lib'); + expect(result).toBe('/node_modules/platform-specific-lib/index.ios.js'); }); test('should resolve nested platform-specific files', async () => { - const { resolve } = await setupTestEnvironment( - { 'platform-lib': loadFixture('platform-lib') }, - { platform: 'android', preferNativePlatform: true } - ); + const { resolve } = await setupTestEnvironment(['platform-lib'], { + platform: 'android', + preferNativePlatform: true, + }); - const result = await resolve('platform-lib/lib/utils'); - expect(result).toBe('/node_modules/platform-lib/lib/utils.android.js'); + const result = await resolve('platform-specific-lib/lib/utils'); + expect(result).toBe( + '/node_modules/platform-specific-lib/lib/utils.android.js' + ); }); }); diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 67d88d670..170f1ba73 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -10,7 +10,10 @@ interface FixtureData { } // Load fixture data from JSON files -export function loadFixture(fixtureName: string): Record { +export function loadFixture(fixtureName: string): { + name: string; + files: Record; +} { const fixtureDir = path.join(__dirname, '__fixtures__'); const fixturePath = path.join(fixtureDir, `${fixtureName}.json`); @@ -18,16 +21,19 @@ export function loadFixture(fixtureName: string): Record { fs.readFileSync(fixturePath, 'utf8') ); - const result: Record = { + const files: Record = { 'package.json': JSON.stringify(fixtureData['package.json']), }; // Create empty files for each path in the files array for (const filePath of fixtureData.files) { - result[filePath] = `// ${filePath}`; + files[filePath] = `// ${filePath}`; } - return result; + return { + name: fixtureData['package.json'].name, + files, + }; } // Simple function to create a package in the virtual filesystem @@ -141,7 +147,7 @@ function createResolvers( // Main setup function - creates filesystem and resolver export async function setupTestEnvironment( - packages: Record>, + fixtures: string[], options: { platform?: string; enablePackageExports?: boolean; @@ -154,9 +160,10 @@ export async function setupTestEnvironment( // Ensure node_modules directory exists first await volume.promises.mkdir('/node_modules', { recursive: true }); - // Create all packages in node_modules - for (const [packageName, files] of Object.entries(packages)) { - await createPackage(volume, `/node_modules/${packageName}`, files); + // Load fixtures and create packages in node_modules + for (const fixtureName of fixtures) { + const { name, files } = loadFixture(fixtureName); + await createPackage(volume, `/node_modules/${name}`, files); } // Create resolvers using the helper function From 5ed7212d4f96815fd7354b9a40c033c42f58054d Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 14:52:19 +0200 Subject: [PATCH 12/21] refactor: assets tests --- .../src/__fixtures__/asset-lib-simple.json | 13 ------------- .../{asset-lib.json => assets.json} | 2 +- .../{platform-lib.json => platforms.json} | 0 .../src/__tests__/asset-resolution.test.ts | 19 +++++-------------- 4 files changed, 6 insertions(+), 28 deletions(-) delete mode 100644 tests/resolver-cases/src/__fixtures__/asset-lib-simple.json rename tests/resolver-cases/src/__fixtures__/{asset-lib.json => assets.json} (89%) rename tests/resolver-cases/src/__fixtures__/{platform-lib.json => platforms.json} (100%) diff --git a/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json b/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json deleted file mode 100644 index b17b52089..000000000 --- a/tests/resolver-cases/src/__fixtures__/asset-lib-simple.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "package.json": { - "name": "asset-lib", - "version": "1.0.0", - "main": "./index.js" - }, - "files": [ - "index.js", - "assets/icon.png", - "assets/logo.jpg", - "assets/video.mp4" - ] -} diff --git a/tests/resolver-cases/src/__fixtures__/asset-lib.json b/tests/resolver-cases/src/__fixtures__/assets.json similarity index 89% rename from tests/resolver-cases/src/__fixtures__/asset-lib.json rename to tests/resolver-cases/src/__fixtures__/assets.json index eb520d3d7..fa661d495 100644 --- a/tests/resolver-cases/src/__fixtures__/asset-lib.json +++ b/tests/resolver-cases/src/__fixtures__/assets.json @@ -7,8 +7,8 @@ "files": [ "index.js", "assets/icon.png", + "assets/icon@1x.png", "assets/icon@2x.png", - "assets/icon@3x.png", "assets/logo.jpg", "assets/video.mp4" ] diff --git a/tests/resolver-cases/src/__fixtures__/platform-lib.json b/tests/resolver-cases/src/__fixtures__/platforms.json similarity index 100% rename from tests/resolver-cases/src/__fixtures__/platform-lib.json rename to tests/resolver-cases/src/__fixtures__/platforms.json diff --git a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts index 8e30c0423..42014c197 100644 --- a/tests/resolver-cases/src/__tests__/asset-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts @@ -2,27 +2,18 @@ import { describe, expect, test } from 'vitest'; import { setupTestEnvironment } from '../test-helpers.js'; describe('Asset Resolution', () => { - test('should resolve base asset when no scale specified and no scaled versions exist', async () => { - const { resolve } = await setupTestEnvironment(['asset-lib-simple'], { + test('should resolve scaled assets', async () => { + const { resolve } = await setupTestEnvironment(['assets'], { platform: 'ios', }); const result = await resolve('asset-lib/assets/icon.png'); - expect(result).toBe('/node_modules/asset-lib/assets/icon.png'); - }); - - test('should resolve 2x scaled assets when available', async () => { - const { resolve } = await setupTestEnvironment(['asset-lib'], { - platform: 'ios', - }); - - // React Native prefers scaled assets when available - const result = await resolve('asset-lib/assets/icon.png'); - expect(result).toBe('/node_modules/asset-lib/assets/icon@2x.png'); + // scales are preffered so @1x.png is prefered over .png + expect(result).toBe('/node_modules/asset-lib/assets/icon@1x.png'); }); test('should resolve different asset formats', async () => { - const { resolve } = await setupTestEnvironment(['asset-lib-simple'], { + const { resolve } = await setupTestEnvironment(['assets'], { platform: 'ios', }); From 12998f649dfaa0a83ab6c0ca7e2af1799b4187cb Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:00:28 +0200 Subject: [PATCH 13/21] refactor: platform tests --- .../src/__fixtures__/ts-platform-lib.json | 16 ---------- .../src/__tests__/platform-resolution.test.ts | 32 +++++-------------- tests/resolver-cases/src/test-helpers.ts | 2 +- 3 files changed, 9 insertions(+), 41 deletions(-) delete mode 100644 tests/resolver-cases/src/__fixtures__/ts-platform-lib.json diff --git a/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json b/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json deleted file mode 100644 index 2f0741532..000000000 --- a/tests/resolver-cases/src/__fixtures__/ts-platform-lib.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "package.json": { - "name": "typescript-platform-lib", - "version": "1.0.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "files": [ - "dist/index.js", - "dist/index.d.ts", - "src/utils.ts", - "src/utils.native.ts", - "src/utils.ios.ts", - "src/utils.android.ts" - ] -} diff --git a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts index 391d4787e..a22c03a5a 100644 --- a/tests/resolver-cases/src/__tests__/platform-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -3,9 +3,8 @@ import { setupTestEnvironment } from '../test-helpers.js'; describe('Platform Resolution', () => { test('should resolve iOS platform files when platform is ios', async () => { - const { resolve } = await setupTestEnvironment(['platform-lib'], { + const { resolve } = await setupTestEnvironment(['platforms'], { platform: 'ios', - preferNativePlatform: true, }); const result = await resolve('platform-specific-lib'); @@ -13,9 +12,8 @@ describe('Platform Resolution', () => { }); test('should resolve Android platform files when platform is android', async () => { - const { resolve } = await setupTestEnvironment(['platform-lib'], { + const { resolve } = await setupTestEnvironment(['platforms'], { platform: 'android', - preferNativePlatform: true, }); const result = await resolve('platform-specific-lib'); @@ -23,41 +21,27 @@ describe('Platform Resolution', () => { }); test('should fallback to native when platform file not found', async () => { - const { resolve } = await setupTestEnvironment(['platform-lib'], { + const { resolve } = await setupTestEnvironment(['platforms'], { platform: 'web', - preferNativePlatform: true, }); const result = await resolve('platform-specific-lib'); expect(result).toBe('/node_modules/platform-specific-lib/index.native.js'); }); - test('should resolve platform-specific TypeScript files', async () => { - const { resolve } = await setupTestEnvironment(['ts-platform-lib'], { - platform: 'ios', - preferNativePlatform: true, - }); - - const result = await resolve('typescript-platform-lib/src/utils'); - expect(result).toBe( - '/node_modules/typescript-platform-lib/src/utils.ios.ts' - ); - }); - - test('should prefer platform over native when preferNativePlatform is false', async () => { - const { resolve } = await setupTestEnvironment(['platform-lib'], { - platform: 'ios', + test('should fallback to default when preferNativePlatform is false', async () => { + const { resolve } = await setupTestEnvironment(['platforms'], { + platform: 'web', preferNativePlatform: false, }); const result = await resolve('platform-specific-lib'); - expect(result).toBe('/node_modules/platform-specific-lib/index.ios.js'); + expect(result).toBe('/node_modules/platform-specific-lib/index.js'); }); test('should resolve nested platform-specific files', async () => { - const { resolve } = await setupTestEnvironment(['platform-lib'], { + const { resolve } = await setupTestEnvironment(['platforms'], { platform: 'android', - preferNativePlatform: true, }); const result = await resolve('platform-specific-lib/lib/utils'); diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 170f1ba73..01b45211c 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -155,7 +155,7 @@ export async function setupTestEnvironment( } = {} ) { const volume = new Volume(); - const platform = options.platform || 'ios'; + const platform = options.platform ?? 'ios'; // Ensure node_modules directory exists first await volume.promises.mkdir('/node_modules', { recursive: true }); From 386eff143d8c6e9f1b56db8e6bb844fc411ab9af Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:12:36 +0200 Subject: [PATCH 14/21] refactor: add fallback resolver --- tests/resolver-cases/src/test-helpers.ts | 38 +++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 01b45211c..65d877836 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -1,11 +1,15 @@ import fs from 'node:fs'; import path from 'node:path'; import { getResolveOptions } from '@callstack/repack'; -import { ResolverFactory } from 'enhanced-resolve'; +import { + type FileSystem, + type Resolver, + ResolverFactory, +} from 'enhanced-resolve'; import { Volume } from 'memfs'; interface FixtureData { - 'package.json': Record; + 'package.json': Record; files: string[]; } @@ -30,10 +34,7 @@ export function loadFixture(fixtureName: string): { files[filePath] = `// ${filePath}`; } - return { - name: fixtureData['package.json'].name, - files, - }; + return { name: fixtureData['package.json'].name, files }; } // Simple function to create a package in the virtual filesystem @@ -61,14 +62,22 @@ async function createPackage( } } +interface Resolvers { + esm: Resolver; + cjs: Resolver; + default: Resolver; +} + // Helper function to resolve modules using the configured resolvers -function createResolveFunction(esmResolver: any, cjsResolver: any) { +function createResolveFunction(resolvers: Resolvers) { return async function resolve( request: string, context = '/app', - dependencyType: 'esm' | 'commonjs' = 'esm' + dependencyType: 'esm' | 'commonjs' | 'default' = 'default' ): Promise { - const resolver = dependencyType === 'esm' ? esmResolver : cjsResolver; + const resolver = + resolvers[dependencyType as keyof Resolvers] ?? resolvers.default; + try { const result = await new Promise((resolve, reject) => { resolver.resolve({}, context, request, {}, (err: any, result: any) => { @@ -123,9 +132,9 @@ function createResolvers( }); // Create resolvers for both ESM and CommonJS - const createResolver = (dependencyType: 'esm' | 'commonjs') => { + const createResolver = (dependencyType: string) => { const specificConditionNames = - resolveOptions.byDependency[dependencyType].conditionNames; + resolveOptions.byDependency[dependencyType]?.conditionNames; return ResolverFactory.createResolver({ mainFields: resolveOptions.mainFields, @@ -134,14 +143,15 @@ function createResolvers( exportsFields: resolveOptions.exportsFields, extensions: resolveOptions.extensions, extensionAlias: resolveOptions.extensionAlias, - fileSystem: volume as any, // Cast to any to work around enhanced-resolve types - symlinks: false, + fileSystem: volume as FileSystem, + symlinks: true, }); }; return { esm: createResolver('esm'), cjs: createResolver('commonjs'), + default: createResolver('unknown'), }; } @@ -170,7 +180,7 @@ export async function setupTestEnvironment( const resolvers = createResolvers(volume, platform, options); return { - resolve: createResolveFunction(resolvers.esm, resolvers.cjs), + resolve: createResolveFunction(resolvers), listFiles: createListFilesFunction(volume), }; } From 5b03b67df395dc830e480f4c7e637ecaaa9b82b3 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:22:10 +0200 Subject: [PATCH 15/21] refactor: cleanup --- tests/resolver-cases/src/test-helpers.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 65d877836..4b9e12b86 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -77,17 +77,11 @@ function createResolveFunction(resolvers: Resolvers) { ): Promise { const resolver = resolvers[dependencyType as keyof Resolvers] ?? resolvers.default; - - try { - const result = await new Promise((resolve, reject) => { - resolver.resolve({}, context, request, {}, (err: any, result: any) => { - err ? reject(err) : resolve(result as string); - }); + return new Promise((resolve) => { + resolver.resolve({}, context, request, {}, (err, result) => { + err ? resolve(null) : resolve(result || null); }); - return result; - } catch { - return null; - } + }); }; } From b3daf3d8652b0774d2af50ff5a538f0dd3e55833 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:35:03 +0200 Subject: [PATCH 16/21] refactor: cleanup & finish --- .../{complex-lib.json => exports.json} | 0 .../src/__tests__/exports-resolution.test.ts | 107 ++++++++++-------- tests/resolver-cases/src/test-helpers.ts | 7 +- 3 files changed, 63 insertions(+), 51 deletions(-) rename tests/resolver-cases/src/__fixtures__/{complex-lib.json => exports.json} (100%) diff --git a/tests/resolver-cases/src/__fixtures__/complex-lib.json b/tests/resolver-cases/src/__fixtures__/exports.json similarity index 100% rename from tests/resolver-cases/src/__fixtures__/complex-lib.json rename to tests/resolver-cases/src/__fixtures__/exports.json diff --git a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts index 5051c9423..6e7cfb83a 100644 --- a/tests/resolver-cases/src/__tests__/exports-resolution.test.ts +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -2,74 +2,87 @@ import { describe, expect, test } from 'vitest'; import { setupTestEnvironment } from '../test-helpers.js'; describe('Package Exports Resolution', () => { - describe('React Strict DOM pattern', () => { - test('should resolve to native version for react-native condition', async () => { - const { resolve } = await setupTestEnvironment(['react-strict-dom'], { + test.fails( + 'should resolve ESM imports with react-native condition', + async () => { + const { resolve } = await setupTestEnvironment(['exports'], { platform: 'ios', enablePackageExports: true, }); - const result = await resolve('react-strict-dom'); - expect(result).toBe( - '/node_modules/react-strict-dom/dist/native/index.js' - ); - }); - - test('should resolve to DOM version when package exports disabled', async () => { - const { resolve } = await setupTestEnvironment(['react-strict-dom'], { - platform: 'ios', - enablePackageExports: false, - }); - - // When exports are disabled, it should fallback to main field (which doesn't exist) - // and then resolve to index.js (which also doesn't exist in this case) - const result = await resolve('react-strict-dom'); - expect(result).toBe(null); - }); - }); - - describe('Complex exports patterns', () => { - test('should resolve ESM imports with react-native condition', async () => { - const { resolve } = await setupTestEnvironment(['complex-lib'], { - platform: 'ios', - enablePackageExports: true, - }); - - const result = await resolve('complex-lib', '/app', 'esm'); + const result = await resolve('complex-lib', 'esm'); expect(result).toBe('/node_modules/complex-lib/esm/index.native.js'); - }); + } + ); - test('should resolve CommonJS requires with react-native condition', async () => { - const { resolve } = await setupTestEnvironment(['complex-lib'], { + test.fails( + 'should resolve CommonJS requires with react-native condition', + async () => { + const { resolve } = await setupTestEnvironment(['exports'], { platform: 'ios', enablePackageExports: true, }); - const result = await resolve('complex-lib', '/app', 'commonjs'); + const result = await resolve('complex-lib', 'cjs'); expect(result).toBe('/node_modules/complex-lib/cjs/index.native.js'); + } + ); + + test('should resolve utils subpath', async () => { + const { resolve } = await setupTestEnvironment(['exports'], { + platform: 'ios', + enablePackageExports: true, }); - test('should resolve utils subpath', async () => { - const { resolve } = await setupTestEnvironment(['complex-lib'], { - platform: 'ios', - enablePackageExports: true, - }); + const esmResult = await resolve('complex-lib/utils', 'esm'); + expect(esmResult).toBe('/node_modules/complex-lib/esm/utils.js'); - const esmResult = await resolve('complex-lib/utils', '/app', 'esm'); - expect(esmResult).toBe('/node_modules/complex-lib/esm/utils.js'); + const cjsResult = await resolve('complex-lib/utils', 'cjs'); + expect(cjsResult).toBe('/node_modules/complex-lib/cjs/utils.js'); + }); - const cjsResult = await resolve('complex-lib/utils', '/app', 'commonjs'); - expect(cjsResult).toBe('/node_modules/complex-lib/cjs/utils.js'); + test('should resolve react-native only exports', async () => { + const { resolve } = await setupTestEnvironment(['exports'], { + platform: 'ios', + enablePackageExports: true, }); - test('should resolve react-native only exports', async () => { - const { resolve } = await setupTestEnvironment(['complex-lib'], { + const result = await resolve('complex-lib/native-only'); + expect(result).toBe('/node_modules/complex-lib/native-specific.js'); + }); + + test.fails( + 'should resolve to native version for react-native condition', + async () => { + const { resolve } = await setupTestEnvironment(['react-strict-dom'], { platform: 'ios', enablePackageExports: true, }); - const result = await resolve('complex-lib/native-only'); - expect(result).toBe('/node_modules/complex-lib/native-specific.js'); + const esmResult = await resolve('react-strict-dom', 'esm'); + expect(esmResult).toBe( + '/node_modules/react-strict-dom/dist/native/index.js' + ); + + const cjsResult = await resolve('react-strict-dom', 'cjs'); + expect(cjsResult).toBe( + '/node_modules/react-strict-dom/dist/native/index.js' + ); + } + ); + + test('should fail to resolve with package exports disabled', async () => { + const { resolve } = await setupTestEnvironment(['react-strict-dom'], { + platform: 'ios', + enablePackageExports: false, }); + + // When exports are disabled, it should fallback to main field (which doesn't exist) + // and then resolve to index.js (which also doesn't exist in this case) + const esmResult = await resolve('react-strict-dom', 'esm'); + expect(esmResult).toBe(null); + + const cjsResult = await resolve('react-strict-dom', 'cjs'); + expect(cjsResult).toBe(null); }); }); diff --git a/tests/resolver-cases/src/test-helpers.ts b/tests/resolver-cases/src/test-helpers.ts index 4b9e12b86..36cc4f9a4 100644 --- a/tests/resolver-cases/src/test-helpers.ts +++ b/tests/resolver-cases/src/test-helpers.ts @@ -72,11 +72,10 @@ interface Resolvers { function createResolveFunction(resolvers: Resolvers) { return async function resolve( request: string, - context = '/app', - dependencyType: 'esm' | 'commonjs' | 'default' = 'default' + dependencyType: 'esm' | 'cjs' | 'default' = 'default', + context = '/' ): Promise { - const resolver = - resolvers[dependencyType as keyof Resolvers] ?? resolvers.default; + const resolver = resolvers[dependencyType] ?? resolvers.default; return new Promise((resolve) => { resolver.resolve({}, context, request, {}, (err, result) => { err ? resolve(null) : resolve(result || null); From 7eab5a99853ea8c71679b754ddd541b5927be306 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:40:14 +0200 Subject: [PATCH 17/21] chore: fix deps --- pnpm-lock.yaml | 9 ++++++--- pnpm-workspace.yaml | 1 + tests/resolver-cases/package.json | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 165309267..a854d228c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,9 @@ catalogs: typescript: specifier: ^5.8.3 version: 5.8.3 + vitest: + specifier: ^2.0.5 + version: 2.0.5 webpack: specifier: ^5.99.5 version: 5.99.5 @@ -715,16 +718,16 @@ importers: specifier: 'catalog:' version: 18.19.41 enhanced-resolve: - specifier: ^5.17.1 + specifier: ^5.18.1 version: 5.18.1 memfs: - specifier: ^4.13.0 + specifier: ^4.11.1 version: 4.17.0 typescript: specifier: 'catalog:' version: 5.8.3 vitest: - specifier: ^2.0.5 + specifier: 'catalog:' version: 2.0.5(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3) website: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d2ee15581..2f9cd63b7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,6 +14,7 @@ catalog: "webpack": ^5.99.5 "react": "19.0.0" "react-native": "0.79.1" + "vitest": ^2.0.5 catalogs: testers: diff --git a/tests/resolver-cases/package.json b/tests/resolver-cases/package.json index e48baa651..71d351458 100644 --- a/tests/resolver-cases/package.json +++ b/tests/resolver-cases/package.json @@ -10,9 +10,9 @@ "devDependencies": { "@callstack/repack": "workspace:*", "@types/node": "catalog:", - "enhanced-resolve": "^5.17.1", - "memfs": "^4.13.0", + "enhanced-resolve": "^5.18.1", + "memfs": "^4.11.1", "typescript": "catalog:", - "vitest": "^2.0.5" + "vitest": "catalog:" } } From 90b4feb8db82fb7d073133dba57546cea4fa81a1 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:40:42 +0200 Subject: [PATCH 18/21] chore: pnpm dedupe --- pnpm-lock.yaml | 60 ++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a854d228c..8f4baeecb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3887,9 +3887,6 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001714: - resolution: {integrity: sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==} - caniuse-lite@1.0.30001716: resolution: {integrity: sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==} @@ -4183,10 +4180,6 @@ packages: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -4724,14 +4717,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.4.4: resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} peerDependencies: @@ -7635,10 +7620,6 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyglobby@0.2.12: - resolution: {integrity: sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -9879,7 +9860,7 @@ snapshots: '@modern-js/utils@2.65.1': dependencies: '@swc/helpers': 0.5.13 - caniuse-lite: 1.0.30001714 + caniuse-lite: 1.0.30001716 lodash: 4.17.21 rslog: 1.2.3 @@ -10877,7 +10858,7 @@ snapshots: dependencies: '@rsbuild/core': 1.3.5 rsbuild-plugin-dts: 0.6.3(@rsbuild/core@1.3.5)(typescript@5.8.3) - tinyglobby: 0.2.12 + tinyglobby: 0.2.13 optionalDependencies: typescript: 5.8.3 transitivePeerDependencies: @@ -11044,7 +11025,7 @@ snapshots: '@module-federation/runtime-tools': 0.8.4 '@rspack/binding': 1.2.2 '@rspack/lite-tapable': 1.0.1 - caniuse-lite: 1.0.30001714 + caniuse-lite: 1.0.30001716 optionalDependencies: '@swc/helpers': 0.5.17 @@ -11053,7 +11034,7 @@ snapshots: '@module-federation/runtime-tools': 0.11.2 '@rspack/binding': 1.3.3 '@rspack/lite-tapable': 1.0.1 - caniuse-lite: 1.0.30001714 + caniuse-lite: 1.0.30001716 optionalDependencies: '@swc/helpers': 0.5.17 @@ -11062,7 +11043,7 @@ snapshots: '@module-federation/runtime-tools': 0.11.2 '@rspack/binding': 1.3.5 '@rspack/lite-tapable': 1.0.1 - caniuse-lite: 1.0.30001714 + caniuse-lite: 1.0.30001716 optionalDependencies: '@swc/helpers': 0.5.17 @@ -11838,7 +11819,7 @@ snapshots: autoprefixer@10.4.20(postcss@8.5.1): dependencies: browserslist: 4.24.4 - caniuse-lite: 1.0.30001714 + caniuse-lite: 1.0.30001716 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 @@ -12033,7 +12014,7 @@ snapshots: browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001714 + caniuse-lite: 1.0.30001716 electron-to-chromium: 1.5.96 node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) @@ -12099,8 +12080,6 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001714: {} - caniuse-lite@1.0.30001716: {} ccount@2.0.1: {} @@ -12398,12 +12377,6 @@ snapshots: dependencies: luxon: 3.5.0 - cross-spawn@7.0.3: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -12823,7 +12796,7 @@ snapshots: execa@5.1.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 6.0.1 human-signals: 2.1.0 is-stream: 2.0.1 @@ -12835,7 +12808,7 @@ snapshots: execa@8.0.1: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 get-stream: 8.0.1 human-signals: 5.0.0 is-stream: 3.0.0 @@ -12848,7 +12821,7 @@ snapshots: execa@9.5.2: dependencies: '@sindresorhus/merge-streams': 4.0.0 - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 figures: 6.1.0 get-stream: 9.0.1 human-signals: 8.0.0 @@ -12969,10 +12942,6 @@ snapshots: dependencies: bser: 2.1.1 - fdir@6.4.3(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - fdir@6.4.4(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -13075,7 +13044,7 @@ snapshots: foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 form-data@4.0.0: @@ -16360,7 +16329,7 @@ snapshots: '@rsbuild/core': 1.3.5 magic-string: 0.30.17 picocolors: 1.1.1 - tinyglobby: 0.2.12 + tinyglobby: 0.2.13 tsconfig-paths: 4.2.0 optionalDependencies: typescript: 5.8.3 @@ -16894,11 +16863,6 @@ snapshots: tinycolor2@1.6.0: {} - tinyglobby@0.2.12: - dependencies: - fdir: 6.4.3(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) From 3a437a6b13006782374098b537148d6f0311a81f Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:42:05 +0200 Subject: [PATCH 19/21] chore: reuse vitest catalog --- apps/tester-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tester-app/package.json b/apps/tester-app/package.json index cc617f9ca..a6fca8889 100644 --- a/apps/tester-app/package.json +++ b/apps/tester-app/package.json @@ -57,7 +57,7 @@ "tailwindcss": "^3.4.17", "terser-webpack-plugin": "catalog:", "typescript": "catalog:", - "vitest": "^2.0.5", + "vitest": "catalog:", "webpack": "catalog:" } } From 1880464721756dbaacfb737cc7a53de54df43db4 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:48:53 +0200 Subject: [PATCH 20/21] chore: rework README.md --- tests/resolver-cases/README.md | 136 +++++++-------------------------- 1 file changed, 26 insertions(+), 110 deletions(-) diff --git a/tests/resolver-cases/README.md b/tests/resolver-cases/README.md index a2f1dc7ac..1c18f7bfa 100644 --- a/tests/resolver-cases/README.md +++ b/tests/resolver-cases/README.md @@ -1,122 +1,38 @@ -# Resolver Test Cases +# resolver-cases-test -A simple testing framework for Repack's module resolution logic. Tests various edge cases and scenarios from the React Native ecosystem without unnecessary abstractions. +## Description -## Philosophy: Simple and Direct +`resolver-cases-test` is a package that tests Repack's module resolution logic using `enhanced-resolve`. It validates various edge cases and scenarios from the React Native ecosystem using JSON fixtures to define package structures. -This package follows a "senior engineer" approach: +## Fixture-Based Approach -- **Zero abstractions**: Direct file structure mapping -- **Visible test data**: Everything you need is right in the test -- **Clear intent**: When you read a test, you know exactly what files exist -- **No duplication**: No repeated fields or interface overhead +Tests use JSON fixtures in `src/__fixtures__/` to define package structures: -## Quick Start - -```typescript -import { setupTestEnvironment } from "../test-helpers.js"; - -// Set up packages as simple file structures -const { resolve } = await setupTestEnvironment( - { - "my-package": { - "package.json": JSON.stringify({ - name: "my-package", - exports: { - ".": { - "react-native": "./native.js", - default: "./web.js", - }, - }, - }), - "native.js": 'export const platform = "native";', - "web.js": 'export const platform = "web";', - }, - }, - { platform: "ios", enablePackageExports: true } -); - -const result = await resolve("my-package"); -expect(result).toBe("/node_modules/my-package/native.js"); -``` - -## What You See Is What You Get - -When reading a test, you can immediately see: - -- What packages exist -- What their `package.json` contains -- What files they have and their content -- No hidden templates or interfaces - -```typescript -const { resolve } = await setupTestEnvironment( - { - "react-lib": { - "package.json": JSON.stringify({ name: "react-lib", main: "./index.js" }), - "index.js": 'export const platform = "web";', - "index.ios.js": 'export const platform = "ios";', - "index.android.js": 'export const platform = "android";', - }, +```json +{ + "package.json": { + "name": "my-package", + "exports": { + ".": { + "react-native": "./native.js", + "default": "./web.js" + } + } }, - { platform: "ios" } -); + "files": ["native.js", "web.js"] +} ``` -## What's Tested - -### Platform Resolution - -- Platform-specific files (`.ios.js`, `.android.js`) -- Native fallbacks (`.native.js`) -- TypeScript platform extensions -- `preferNativePlatform` behavior - -### Package Exports - -- Conditional exports with `react-native` condition -- ESM vs CommonJS resolution differences -- Subpath exports +## Usage -### Asset Resolution - -- Scaled asset handling (@2x, @3x) -- Extension alias behavior - -## API - -### `setupTestEnvironment(packages, options)` - -Creates a virtual filesystem with packages and returns a resolver. - -**Parameters:** - -- `packages` - Object where keys are package names, values are file structures -- `options` - Resolver options (platform, enablePackageExports, etc.) - -**Returns:** - -- `resolve(request, context?, dependencyType?)` - Resolve a module request -- `volume` - Direct access to memfs Volume -- `listFiles()` - Debug helper to see all files +```ts +import { setupTestEnvironment } from "../test-helpers.js"; -## Structure +const { resolve } = await setupTestEnvironment(["platforms"], { + platform: "ios", + enablePackageExports: true, +}); +const result = await resolve("platform-specific-lib"); +expect(result).toBe("/node_modules/platform-specific-lib/index.ios.js"); ``` -src/ -├── test-helpers.ts # Single helper file with everything -└── __tests__/ - ├── platform-resolution.test.ts - ├── exports-resolution.test.ts - └── asset-resolution.test.ts -``` - -## Why This Approach? - -1. **No Interfaces**: Package structure is just `Record>` -2. **No Duplication**: No repeated name/version fields -3. **Immediate Understanding**: Test setup == actual filesystem structure -4. **Easy Debugging**: `listFiles()` shows exactly what you created -5. **Copy-Paste Friendly**: Easy to copy real package.json content - -When a test fails, you know exactly what files exist and what they contain, all visible right in the test. From 70f603255a566150509c7c459a79639ffef3fbf4 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Sun, 29 Jun 2025 15:49:54 +0200 Subject: [PATCH 21/21] chore: update lockfile --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f4baeecb..e953abce6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,7 +211,7 @@ importers: specifier: 'catalog:' version: 5.8.3 vitest: - specifier: ^2.0.5 + specifier: 'catalog:' version: 2.0.5(@types/node@20.14.11)(lightningcss@1.28.2)(terser@5.31.3) webpack: specifier: 'catalog:'