Skip to content

Commit

Permalink
feat(exports): Set up ESM first (#27)
Browse files Browse the repository at this point in the history
* feat(exports): Set up ESM first

* Switch to VItest

* Update ESLint

* Move register files to a single TS

* Enhance replace(..), fix bad RendererProxy mock, and create a RN Vitest environment

* Revert README.md changes

* Polishing
  • Loading branch information
JoseLion authored Jun 30, 2024
1 parent febd077 commit eb8b681
Show file tree
Hide file tree
Showing 22 changed files with 1,885 additions and 622 deletions.
15 changes: 11 additions & 4 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// @ts-check
import path from "path";
import { fileURLToPath } from "url";

import { fixupPluginRules } from "@eslint/compat";
import { FlatCompat } from "@eslint/eslintrc";
import eslintJs from "@eslint/js";
Expand All @@ -11,9 +14,6 @@ import sonarjs from "eslint-plugin-sonarjs";
import globals from "globals";
import eslintTs from "typescript-eslint";

import path from "path";
import { fileURLToPath } from "url";

const project = "./tsconfig.json";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
Expand Down Expand Up @@ -214,7 +214,14 @@ export default eslintTs.config(
order: "asc",
orderImportKind: "asc",
},
groups: ["external", "parent", "sibling", "type"],
groups: [
"builtin",
["external", "internal"],
"parent",
"sibling",
"index",
"type",
],
"newlines-between": "always",
}],
"jsdoc/check-alignment": "error",
Expand Down
53 changes: 37 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,71 +13,92 @@
"mocks",
"mock"
],
"main": "./dist/main.js",
"type": "module",
"source": "./src/main.ts",
"main": "./dist/main.cjs",
"module": "./dist/main.js",
"types": "./dist/main.d.ts",
"sideEffects": true,
"exports": {
".": {
"import": "./dist/main.js",
"require": "./dist/main.cjs",
"types": "./dist/main.d.ts",
"default": "./dist/main.js"
},
"./register": {
"import": "./dist/register.js",
"require": "./dist/register.cjs",
"types": "./dist/register.d.ts",
"default": "./dist/register.js"
},
"./package.json": "./package.json"
},
"files": [
"dist/",
"src/",
"register.js"
"./dist",
"./src/",
"./package.json"
],
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsc -p tsconfig.prod.json",
"check": "yarn compile && yarn lint && yarn test",
"build": "vite build",
"check": "yarn compile && yarn lint && yarn test --run",
"compile": "tsc",
"lint": "eslint .",
"release": "semantic-release",
"test": "NODE_ENV=test mocha"
"test": "vitest"
},
"packageManager": "yarn@4.3.1",
"dependencies": {
"@babel/core": "^7.24.7",
"@babel/register": "^7.24.6",
"babel-plugin-module-resolver": "^5.0.2",
"dot-prop-immutable": "^2.1.1",
"pino": "^9.2.0",
"pino-pretty": "^11.2.1",
"ts-pattern": "^5.2.0"
},
"devDependencies": {
"@assertive-ts/core": "^2.1.0",
"@eslint/compat": "^1.1.0",
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.5.0",
"@eslint/js": "^9.6.0",
"@react-native/babel-preset": "^0.74.84",
"@stylistic/eslint-plugin": "^2.2.2",
"@stylistic/eslint-plugin": "^2.3.0",
"@testing-library/react-native": "^12.5.1",
"@types/babel__core": "^7.20.5",
"@types/babel__register": "^7.17.3",
"@types/eslint__eslintrc": "^2.1.1",
"@types/eslint__js": "^8.42.3",
"@types/mocha": "^10.0.7",
"@types/node": "^20.14.8",
"@types/node": "^20.14.9",
"@types/react": "^18.3.3",
"@types/react-test-renderer": "^18.3.0",
"@types/sinon": "^17.0.3",
"eslint": "^9.5.0",
"eslint": "^9.6.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-deprecation": "^3.0.0",
"eslint-plugin-etc": "^2.0.3",
"eslint-plugin-extra-rules": "^0.0.0-development",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.4.0",
"eslint-plugin-jsdoc": "^48.5.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-sonarjs": "^1.0.3",
"globals": "^15.6.0",
"mocha": "^10.5.0",
"react": "18.3.1",
"react-native": "^0.74.2",
"react-native-svg": "^15.3.0",
"react-test-renderer": "^18.3.1",
"semantic-release": "^24.0.0",
"semantic-release-yarn": "^3.0.2",
"sinon": "^18.0.0",
"ts-node": "^10.9.2",
"tslib": "^2.6.3",
"typescript": "^5.5.2",
"typescript-eslint": "^7.14.1"
"typescript-eslint": "^7.14.1",
"vite": "^5.3.2",
"vite-plugin-dts": "^3.9.1",
"vitest": "^1.6.0"
},
"peerDependencies": {
"@react-native/babel-preset": ">=0.73.18",
Expand Down
8 changes: 0 additions & 8 deletions register.js

This file was deleted.

19 changes: 7 additions & 12 deletions src/helpers/commons.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import path from "path";

type ExportsLike = object | { default?: unknown; };

/**
* A simple no-operation function
*/
Expand All @@ -13,12 +15,15 @@ export function noop(): void {
* @param modulePath the path to the module
* @param exports the exports to replace
*/
export function replace<T>(modulePath: string, exports: T): void {
export function replace<T extends ExportsLike>(modulePath: string, factory: () => T): void {
const id = resolveId(modulePath);
const exports = factory();

require.cache[id] = {
children: [],
exports,
exports: "default" in exports
? { __esModule: true, ...exports }
: exports,
filename: id,
id,
isPreloading: false,
Expand All @@ -30,16 +35,6 @@ export function replace<T>(modulePath: string, exports: T): void {
};
}

/**
* Replaces am ESModule with a given `exports` value or another module path.
*
* @param modulePath the path to the ESModule
* @param defaultExport the default export to replace
*/
export function replaceEsm<T>(modulePath: string, defaultExport: T): void {
replace(modulePath, { __esModule: true, default: defaultExport });
}

function resolveId(modulePath: string): string {
try {
return require.resolve(modulePath);
Expand Down
4 changes: 4 additions & 0 deletions src/lib/Core/RendererProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line import/no-namespace
import * as RendererImplementation from "react-native/Libraries/ReactNative/RendererImplementation";

export const RendererProxyMock = RendererImplementation;
24 changes: 11 additions & 13 deletions src/lib/coreMocks.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { noop, replace, replaceEsm } from "../helpers/commons";
import { noop, replace } from "../helpers/commons";
import { mockNativeComponent } from "../helpers/mockNativeComponent";

import { LinkingMock } from "./Components/Linking";
import { NativeComponentRegistryMock } from "./Core/NativeComponentRegistry";
import { NativeModulesMock } from "./Core/NativeModules";
import { RendererProxyMock } from "./Core/RendererProxy";
import { UIManagerMock } from "./Core/UIManager";
import { verifyComponentAttrEqMock } from "./Core/verifyComponentAttributeEquivalence";

Object.assign(global, { jest: { fn: () => noop } });

replace("react-native/Libraries/Core/InitializeCore", { });
replace("react-native/Libraries/Core/NativeExceptionsManager", { });
replace("react-native/Libraries/ReactNative/UIManager", UIManagerMock);
replace("react-native/Libraries/Linking/Linking", LinkingMock);
replace("react-native/Libraries/BatchedBridge/NativeModules", NativeModulesMock);
replace("react-native/Libraries/NativeComponent/NativeComponentRegistry", NativeComponentRegistryMock);
replaceEsm("react-native/Libraries/ReactNative/requireNativeComponent", mockNativeComponent);
replace("react-native/Libraries/Utilities/verifyComponentAttributeEquivalence", verifyComponentAttrEqMock);
replace(
"react-native/Libraries/ReactNative/RendererProxy",
"react-native/Libraries/ReactNative/RendererImplementation",
);
replace("react-native/Libraries/Core/InitializeCore", () => ({ }));
replace("react-native/Libraries/Core/NativeExceptionsManager", () => ({ }));
replace("react-native/Libraries/ReactNative/UIManager", () => UIManagerMock);
replace("react-native/Libraries/Linking/Linking", () => LinkingMock);
replace("react-native/Libraries/BatchedBridge/NativeModules", () => NativeModulesMock);
replace("react-native/Libraries/NativeComponent/NativeComponentRegistry", () => NativeComponentRegistryMock);
replace("react-native/Libraries/ReactNative/requireNativeComponent", () => ({ default: mockNativeComponent }));
replace("react-native/Libraries/Utilities/verifyComponentAttributeEquivalence", () => verifyComponentAttrEqMock);
replace("react-native/Libraries/ReactNative/RendererProxy", () => RendererProxyMock);
4 changes: 2 additions & 2 deletions src/lib/mockNative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function mockNative(type: NativeKey, methods: Partial<AllNativeMethods |
.with("TextInput", () => mockComponent(Comp, Object.assign({ }, textInputMethodsMock, methods)))
.otherwise(() => mockComponent(Comp, Object.assign({ }, nativeMethodsMock, methods)));

replace(path, type === "ActivityIndicator" ? { default: Mock } : Mock);
replace(path, () => type === "ActivityIndicator" ? { default: Mock } : Mock);
MOCKS.add(type);
}

Expand All @@ -74,7 +74,7 @@ export function restoreNativeMocks(): void {
.with("TextInput", () => mockComponent(Comp, textInputMethodsMock))
.otherwise(() => mockComponent(Comp, nativeMethodsMock));

replace(path, type === "ActivityIndicator" ? { default: Mock } : Mock);
replace(path, () => type === "ActivityIndicator" ? { default: Mock } : Mock);
});

MOCKS.clear();
Expand Down
12 changes: 7 additions & 5 deletions src/lib/polyfills.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import "@react-native/js-polyfills/error-guard";

global.IS_REACT_ACT_ENVIRONMENT = true;
import regeneratorRuntime from "regenerator-runtime/runtime";

// Suppress the `react-test-renderer` warnings until New Architecture and legacy
// mode are no longer supported by React Native.
// @ts-expect-error type not defined
global.IS_REACT_NATIVE_TEST_ENVIRONMENT = true;
Object.assign(global, {
IS_REACT_ACT_ENVIRONMENT: true,
IS_REACT_NATIVE_TEST_ENVIRONMENT: true,
});

Object.defineProperties(global, {
__DEV__: {
Expand Down Expand Up @@ -37,7 +39,7 @@ Object.defineProperties(global, {
regeneratorRuntime: {
configurable: true,
enumerable: true,
value: require("regenerator-runtime/runtime") as unknown,
value: regeneratorRuntime,
writable: true,
},
requestAnimationFrame: {
Expand Down
38 changes: 38 additions & 0 deletions src/load.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import "./lib/babelRegister";
import "./lib/polyfills";
import "./lib/coreMocks";

import { replace } from "./helpers/commons";
import { AnimatedMock } from "./lib/Animated/AnimatedMock";
import { AccessibilityInfoMock } from "./lib/Components/AccessibilityInfo";
import { ActivityIndicatorMock } from "./lib/Components/ActivityIndicator";
import { AppStateMock } from "./lib/Components/AppState";
import { ClipboardMock } from "./lib/Components/Clipboard";
import { ImageMock } from "./lib/Components/Image";
import { ModalMock } from "./lib/Components/Modal";
import { RefreshControlMock } from "./lib/Components/RefreshControl";
import { ScrollViewMock } from "./lib/Components/ScrollView";
import { TextMock } from "./lib/Components/Text";
import { TextInputMock } from "./lib/Components/TextInput";
import { VibrationMock } from "./lib/Components/Vibration";
import { ViewMock } from "./lib/Components/View";
import { ViewNativeComponentMock } from "./lib/Components/ViewNativeComponent";

const libs = "react-native/Libraries";

replace(`${libs}/Image/Image`, () => ImageMock);
replace(`${libs}/Text/Text`, () => TextMock);
replace(`${libs}/Components/TextInput/TextInput`, () => TextInputMock);
replace(`${libs}/Modal/Modal`, () => ModalMock);
replace(`${libs}/Components/AccessibilityInfo/AccessibilityInfo`, () => ({ default: AccessibilityInfoMock }));
replace(`${libs}/Components/Clipboard/Clipboard`, () => ClipboardMock);
replace(`${libs}/Components/RefreshControl/RefreshControl`, () => RefreshControlMock);
replace(`${libs}/Components/ScrollView/ScrollView`, () => ScrollViewMock);
replace(`${libs}/Components/ActivityIndicator/ActivityIndicator`, () => ({ default: ActivityIndicatorMock }));
replace(`${libs}/AppState/AppState`, () => AppStateMock);
replace(`${libs}/Vibration/Vibration`, () => VibrationMock);
replace(`${libs}/Components/View/View`, () => ViewMock);
replace(`${libs}/Components/View/ViewNativeComponent`, () => ViewNativeComponentMock);
replace(`${libs}/Animated/Animated`, () => ({ default: AnimatedMock }));

export { };
45 changes: 13 additions & 32 deletions src/register.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,15 @@
import "./lib/babelRegister";
import "./lib/polyfills";
import "./lib/coreMocks";
import { createRequire } from "module";

import { replace, replaceEsm } from "./helpers/commons";
import { AnimatedMock } from "./lib/Animated/AnimatedMock";
import { AccessibilityInfoMock } from "./lib/Components/AccessibilityInfo";
import { ActivityIndicatorMock } from "./lib/Components/ActivityIndicator";
import { AppStateMock } from "./lib/Components/AppState";
import { ClipboardMock } from "./lib/Components/Clipboard";
import { ImageMock } from "./lib/Components/Image";
import { ModalMock } from "./lib/Components/Modal";
import { RefreshControlMock } from "./lib/Components/RefreshControl";
import { ScrollViewMock } from "./lib/Components/ScrollView";
import { TextMock } from "./lib/Components/Text";
import { TextInputMock } from "./lib/Components/TextInput";
import { VibrationMock } from "./lib/Components/Vibration";
import { ViewMock } from "./lib/Components/View";
import { ViewNativeComponentMock } from "./lib/Components/ViewNativeComponent";
import pino from "pino";
import pinoPretty from "pino-pretty";

replace("react-native/Libraries/Image/Image", ImageMock);
replace("react-native/Libraries/Text/Text", TextMock);
replace("react-native/Libraries/Components/TextInput/TextInput", TextInputMock);
replace("react-native/Libraries/Modal/Modal", ModalMock);
replaceEsm("react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo", AccessibilityInfoMock);
replace("react-native/Libraries/Components/Clipboard/Clipboard", ClipboardMock);
replace("react-native/Libraries/Components/RefreshControl/RefreshControl", RefreshControlMock);
replace("react-native/Libraries/Components/ScrollView/ScrollView", ScrollViewMock);
replaceEsm("react-native/Libraries/Components/ActivityIndicator/ActivityIndicator", ActivityIndicatorMock);
replace("react-native/Libraries/AppState/AppState", AppStateMock);
replace("react-native/Libraries/Vibration/Vibration", VibrationMock);
replace("react-native/Libraries/Components/View/View", ViewMock);
replace("react-native/Libraries/Components/View/ViewNativeComponent", ViewNativeComponentMock);
replaceEsm("react-native/Libraries/Animated/Animated", AnimatedMock);
const start = Date.now();
const logger = pino(pinoPretty({ colorize: true }));
const require = createRequire(import.meta.url);

require("./load.cjs");

const end = Date.now();
const diff = (end - start) / 1000;

logger.info(`React Native testing mocks registered! (${diff}s)`);
12 changes: 12 additions & 0 deletions test/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Environment } from "vitest";

const reactNativeEnv: Environment = {
name: "react-native",
setup: async () => {
await import("../src/load");
return { teardown: () => undefined };
},
transformMode: "ssr",
};

export default reactNativeEnv;
15 changes: 0 additions & 15 deletions test/hooks.ts

This file was deleted.

Loading

0 comments on commit eb8b681

Please sign in to comment.