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:" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a3d868f6..e953abce6 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 @@ -208,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:' @@ -706,6 +709,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.18.1 + version: 5.18.1 + memfs: + specifier: ^4.11.1 + version: 4.17.0 + typescript: + specifier: 'catalog:' + version: 5.8.3 + vitest: + specifier: 'catalog:' + version: 2.0.5(@types/node@18.19.41)(lightningcss@1.28.2)(terser@5.31.3) + website: dependencies: '@callstack/rspress-theme': @@ -3863,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==} @@ -4159,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'} @@ -4700,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: @@ -7611,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'} @@ -9855,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 @@ -10853,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: @@ -11020,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 @@ -11029,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 @@ -11038,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 @@ -11814,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 @@ -12009,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) @@ -12075,8 +12080,6 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001714: {} - caniuse-lite@1.0.30001716: {} ccount@2.0.1: {} @@ -12374,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 @@ -12799,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 @@ -12811,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 @@ -12824,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 @@ -12945,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 @@ -13051,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: @@ -16336,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 @@ -16870,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) @@ -17143,6 +17131,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 +17167,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 +17189,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/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/README.md b/tests/resolver-cases/README.md new file mode 100644 index 000000000..1c18f7bfa --- /dev/null +++ b/tests/resolver-cases/README.md @@ -0,0 +1,38 @@ +# resolver-cases-test + +## Description + +`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. + +## Fixture-Based Approach + +Tests use JSON fixtures in `src/__fixtures__/` to define package structures: + +```json +{ + "package.json": { + "name": "my-package", + "exports": { + ".": { + "react-native": "./native.js", + "default": "./web.js" + } + } + }, + "files": ["native.js", "web.js"] +} +``` + +## Usage + +```ts +import { setupTestEnvironment } from "../test-helpers.js"; + +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"); +``` diff --git a/tests/resolver-cases/package.json b/tests/resolver-cases/package.json new file mode 100644 index 000000000..71d351458 --- /dev/null +++ b/tests/resolver-cases/package.json @@ -0,0 +1,18 @@ +{ + "name": "resolver-cases-test", + "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.18.1", + "memfs": "^4.11.1", + "typescript": "catalog:", + "vitest": "catalog:" + } +} diff --git a/tests/resolver-cases/src/__fixtures__/assets.json b/tests/resolver-cases/src/__fixtures__/assets.json new file mode 100644 index 000000000..fa661d495 --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/assets.json @@ -0,0 +1,15 @@ +{ + "package.json": { + "name": "asset-lib", + "version": "1.0.0", + "main": "./index.js" + }, + "files": [ + "index.js", + "assets/icon.png", + "assets/icon@1x.png", + "assets/icon@2x.png", + "assets/logo.jpg", + "assets/video.mp4" + ] +} diff --git a/tests/resolver-cases/src/__fixtures__/exports.json b/tests/resolver-cases/src/__fixtures__/exports.json new file mode 100644 index 000000000..650572bd7 --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/exports.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", + "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__/platforms.json b/tests/resolver-cases/src/__fixtures__/platforms.json new file mode 100644 index 000000000..c30355b58 --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/platforms.json @@ -0,0 +1,17 @@ +{ + "package.json": { + "name": "platform-specific-lib", + "version": "1.0.0", + "main": "./index" + }, + "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 new file mode 100644 index 000000000..a85a48fa6 --- /dev/null +++ b/tests/resolver-cases/src/__fixtures__/react-strict-dom.json @@ -0,0 +1,29 @@ +{ + "package.json": { + "name": "react-strict-dom", + "version": "0.0.28", + "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", + "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/__tests__/asset-resolution.test.ts b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts new file mode 100644 index 000000000..42014c197 --- /dev/null +++ b/tests/resolver-cases/src/__tests__/asset-resolution.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, test } from 'vitest'; +import { setupTestEnvironment } from '../test-helpers.js'; + +describe('Asset Resolution', () => { + test('should resolve scaled assets', async () => { + const { resolve } = await setupTestEnvironment(['assets'], { + platform: 'ios', + }); + + const result = await resolve('asset-lib/assets/icon.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(['assets'], { + platform: 'ios', + }); + + const jpgResult = await resolve('asset-lib/assets/logo.jpg'); + expect(jpgResult).toBe('/node_modules/asset-lib/assets/logo.jpg'); + + 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 new file mode 100644 index 000000000..6e7cfb83a --- /dev/null +++ b/tests/resolver-cases/src/__tests__/exports-resolution.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from 'vitest'; +import { setupTestEnvironment } from '../test-helpers.js'; + +describe('Package Exports Resolution', () => { + test.fails( + 'should resolve ESM imports with react-native condition', + async () => { + const { resolve } = await setupTestEnvironment(['exports'], { + platform: 'ios', + enablePackageExports: true, + }); + + const result = await resolve('complex-lib', 'esm'); + expect(result).toBe('/node_modules/complex-lib/esm/index.native.js'); + } + ); + + 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', '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, + }); + + const esmResult = await resolve('complex-lib/utils', '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'); + }); + + test('should resolve react-native only exports', async () => { + const { resolve } = await setupTestEnvironment(['exports'], { + platform: 'ios', + enablePackageExports: true, + }); + + 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 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/__tests__/platform-resolution.test.ts b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts new file mode 100644 index 000000000..a22c03a5a --- /dev/null +++ b/tests/resolver-cases/src/__tests__/platform-resolution.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from 'vitest'; +import { setupTestEnvironment } from '../test-helpers.js'; + +describe('Platform Resolution', () => { + test('should resolve iOS platform files when platform is ios', async () => { + const { resolve } = await setupTestEnvironment(['platforms'], { + platform: 'ios', + }); + + 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(['platforms'], { + platform: 'android', + }); + + 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(['platforms'], { + platform: 'web', + }); + + const result = await resolve('platform-specific-lib'); + expect(result).toBe('/node_modules/platform-specific-lib/index.native.js'); + }); + + 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.js'); + }); + + test('should resolve nested platform-specific files', async () => { + const { resolve } = await setupTestEnvironment(['platforms'], { + platform: 'android', + }); + + 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 new file mode 100644 index 000000000..36cc4f9a4 --- /dev/null +++ b/tests/resolver-cases/src/test-helpers.ts @@ -0,0 +1,179 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { getResolveOptions } from '@callstack/repack'; +import { + type FileSystem, + type Resolver, + ResolverFactory, +} from 'enhanced-resolve'; +import { Volume } from 'memfs'; + +interface FixtureData { + 'package.json': Record; + files: string[]; +} + +// Load fixture data from JSON files +export function loadFixture(fixtureName: string): { + name: string; + files: Record; +} { + const fixtureDir = path.join(__dirname, '__fixtures__'); + const fixturePath = path.join(fixtureDir, `${fixtureName}.json`); + + const fixtureData: FixtureData = JSON.parse( + fs.readFileSync(fixturePath, 'utf8') + ); + + 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) { + files[filePath] = `// ${filePath}`; + } + + return { name: fixtureData['package.json'].name, files }; +} + +// 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); + } +} + +interface Resolvers { + esm: Resolver; + cjs: Resolver; + default: Resolver; +} + +// Helper function to resolve modules using the configured resolvers +function createResolveFunction(resolvers: Resolvers) { + return async function resolve( + request: string, + dependencyType: 'esm' | 'cjs' | 'default' = 'default', + context = '/' + ): Promise { + const resolver = resolvers[dependencyType] ?? resolvers.default; + return new Promise((resolve) => { + resolver.resolve({}, context, request, {}, (err, result) => { + err ? resolve(null) : resolve(result || null); + }); + }); + }; +} + +// 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, + preferNativePlatform: options.preferNativePlatform, + }); + + // Create resolvers for both ESM and CommonJS + const createResolver = (dependencyType: string) => { + const specificConditionNames = + resolveOptions.byDependency[dependencyType]?.conditionNames; + + return ResolverFactory.createResolver({ + mainFields: resolveOptions.mainFields, + aliasFields: resolveOptions.aliasFields, + conditionNames: specificConditionNames ?? resolveOptions.conditionNames, + exportsFields: resolveOptions.exportsFields, + extensions: resolveOptions.extensions, + extensionAlias: resolveOptions.extensionAlias, + fileSystem: volume as FileSystem, + symlinks: true, + }); + }; + + return { + esm: createResolver('esm'), + cjs: createResolver('commonjs'), + default: createResolver('unknown'), + }; +} + +// Main setup function - creates filesystem and resolver +export async function setupTestEnvironment( + fixtures: string[], + 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 }); + + // 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 + const resolvers = createResolvers(volume, platform, options); + + return { + resolve: createResolveFunction(resolvers), + listFiles: createListFilesFunction(volume), + }; +} 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, + }, +});