Skip to content

Commit

Permalink
feat(jest): add @react-native-esbuild/jest
Browse files Browse the repository at this point in the history
  • Loading branch information
leegeunhyeok committed Oct 24, 2023
1 parent cda3e36 commit abbda25
Show file tree
Hide file tree
Showing 29 changed files with 455 additions and 29 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ module.exports = {
'no-console': 'off',
},
},
{
files: ['packages/jest/lib/**/*.ts'],
rules: {
'import/no-default-export': 'off',
'import/no-named-as-default': 'off',
},
},
{
files: ['example/**/*'],
rules: {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
46 changes: 46 additions & 0 deletions example/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { resolve } = require('node:path');

const project = resolve(__dirname, 'tsconfig.json');

/** @type { import('eslint').ESLint.ConfigData } */
module.exports = {
root: true,
env: {
node: true,
},
plugins: ['prettier'],
extends: [
require.resolve('@vercel/style-guide/eslint/node'),
require.resolve('@vercel/style-guide/eslint/typescript'),
],
parserOptions: {
project,
},
settings: {
'import/resolver': {
typescript: {
project,
},
},
},
overrides: [
{
files: ['*.ts?(x)', '*.js?(x)'],
rules: {
semi: ['error', 'always'],
quotes: ['error', 'single'],
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': 'off',
'no-console': 'off',
'unicorn/filename-case': 'off',
'prettier/prettier': 'error',
},
},
{
files: ['*.test.ts?(x)', '*.spec.js?(x)'],
rules: {
'import/no-named-as-default-member': 'off',
},
},
],
};
1 change: 1 addition & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ yarn-error.log

# @react-native-esbuild
.rne
.swc

# @react-native-esbuild for web
public/
26 changes: 10 additions & 16 deletions example/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
/**
* @format
*/

import 'react-native';
import React from 'react';
import App from '../src/App';

// Note: import explicitly to use the types shiped with jest.
import {it} from '@jest/globals';

// Note: test renderer must be required after react-native.
import renderer from 'react-test-renderer';

it('renders correctly', () => {
renderer.create(<App />);
import { describe, it } from '@jest/globals';
import renderer, { act } from 'react-test-renderer';
import { App } from '../src/App';

describe('App', () => {
it('renders correctly', async () => {
await act(() => {
renderer.create(<App />);
});
});
});
5 changes: 4 additions & 1 deletion example/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
plugins: ['react-native-reanimated/plugin'],
plugins: [
['@babel/plugin-transform-private-methods', { loose: true }],
'react-native-reanimated/plugin',
],
};
25 changes: 25 additions & 0 deletions example/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
const TRANSFORM_PACKAGE = [
'react-native',
'jest-react-native',
'@react-native',
'@react-native-community',
'@react-navigation',
'@expo/html-elements',
'dripsy',
];

/**
* @type {import('jest').Config}
*/
module.exports = {
preset: 'react-native',
transform: {
'^.+\\.(t|j)sx?$': '@react-native-esbuild/jest',
},
transformIgnorePatterns: [`node_modules/(?!${TRANSFORM_PACKAGE.join('|')})/`],
testPathIgnorePatterns: ['dist'],
coveragePathIgnorePatterns: ['node_modules'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!**/*.d.ts',
'!**/index.ts',
],
setupFilesAfterEnv: ['./tests/setup.ts'],
};
4 changes: 4 additions & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/plugin-transform-export-namespace-from": "^7.22.11",
"@babel/plugin-transform-private-methods": "^7.22.5",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@jest/globals": "^29.7.0",
"@react-native-community/cli": "^11.3.6",
"@react-native-community/cli-platform-android": "^11.3.6",
"@react-native-esbuild/cli": "workspace:^",
"@react-native-esbuild/jest": "workspace:^",
"@react-native/assets-registry": "^0.73.0",
"@react-native/codegen": "^0.72.6",
"@react-native/gradle-plugin": "^0.72.11",
"@react-native/metro-config": "^0.72.11",
"@react-navigation/devtools": "^6.0.19",
"@swc/jest": "^0.2.29",
"@tsconfig/react-native": "^3.0.0",
"@types/react": "^18.0.24",
"@types/react-test-renderer": "^18.0.0",
Expand Down
8 changes: 8 additions & 0 deletions example/tests/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'react-native-gesture-handler/jestSetup';
import { setUpTests } from 'react-native-reanimated';

setUpTests();

jest.mock('@react-navigation/devtools', () => ({
useFlipper: jest.fn(),
}));
2 changes: 1 addition & 1 deletion example/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"rootDir": ".",
"jsx": "react-native"
},
"include": ["src/**/*"]
"include": ["src/**/*", "tests/*", "__tests__/**/*"]
}
8 changes: 4 additions & 4 deletions packages/core/lib/bundler/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
}
}

public static getConfig(): Config {
return getConfigFromGlobal();
}

public static setGlobalLogLevel(logLevel: LogLevel): void {
Logger.setGlobalLogLevel(logLevel);
}
Expand Down Expand Up @@ -443,10 +447,6 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
return buildTask.handler.task;
}

public getConfig(): Config {
return this.config;
}

public getRoot(): string {
return this.root;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/jest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# preset
jest-preset.js
42 changes: 42 additions & 0 deletions packages/jest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# `@react-native-esbuild/jest`

> react-native preset for jest powered by @react-native-esbuild
## Usage

```js
exports.default = {
preset: 'react-native',
transform: {
'^.+\\.(t|j)sx?$': '@react-native-esbuild/jest',
},
};

// With transformer options(for enable custom instrument).
exports.default = {
preset: 'react-native',
transform: {
'^.+\\.(t|j)sx?$': ['@react-native-esbuild/jest', /* TransformerConfig */],
},
};
```

```ts
/**
* @see {@link https://github.com/kwonoj/swc-plugin-coverage-instrument}
*/
interface TransformerConfig {
experimental?: {
customCoverageInstrumentation?: {
coverageVariable?: string;
compact: boolean;
reportLogic: boolean;
ignoreClassMethods: string[];
instrumentLog?: {
level: string;
enableTrace: boolean;
};
};
};
}
```
9 changes: 9 additions & 0 deletions packages/jest/build/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const esbuild = require('esbuild');
const { getEsbuildBaseOptions } = require('../../../shared');

const buildOptions = getEsbuildBaseOptions(__dirname);

esbuild.build(buildOptions).catch((error) => {
console.error(error);
process.exit(1);
});
8 changes: 8 additions & 0 deletions packages/jest/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { TransformerFactory } from '@jest/transform';
import { createTransformer } from './transformer';

const factory: TransformerFactory<ReturnType<typeof createTransformer>> = {
createTransformer,
};

export default factory;
114 changes: 114 additions & 0 deletions packages/jest/lib/transformer/createTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type {
Transformer,
TransformOptions,
TransformedSource,
} from '@jest/transform';
import getCacheKeyFunction from '@jest/create-cache-key-function';
import md5 from 'md5';
import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
import {
SyncTransformPipeline,
AsyncTransformPipeline,
swcPresets,
} from '@react-native-esbuild/transformer';
import { getReactNativeInitializeCore } from '@react-native-esbuild/internal';
import type { TransformerConfig } from '../types';
import pkg from '../../package.json';

const DUMMY_ENTRY = '';
const DUMMY_ESBUILD_VALUE = '';
const ROOT = process.cwd();

ReactNativeEsbuildBundler.bootstrap();

export const createTransformer = (config: TransformerConfig): Transformer => {
const cacheKeyFunction = getCacheKeyFunction([], [pkg.version]);
const { transformer } = ReactNativeEsbuildBundler.getConfig();
const instrumentEnabled = Boolean(
config.experimental?.customCoverageInstrumentation,
);
const swcExperimentalOptions = instrumentEnabled
? config.experimental
: undefined;

const syncTransformPipeline = new SyncTransformPipeline.builder(
ROOT,
DUMMY_ENTRY,
{
swc: swcPresets.getJestOptions({
module: 'cjs',
experimental: swcExperimentalOptions,
}),
},
)
.setInjectScripts([getReactNativeInitializeCore(ROOT)])
.setFullyTransformPackages(transformer?.fullyTransformPackageNames ?? [])
.setStripFlowPackages(transformer?.stripFlowPackageNames ?? [])
.setAdditionalBabelTransformRules(
transformer?.additionalTransformRules?.babel ?? [],
)
.setAdditionalSwcTransformRules(
transformer?.additionalTransformRules?.swc ?? [],
)
.build();

const asyncTransformPipeline = new AsyncTransformPipeline.builder(
ROOT,
DUMMY_ENTRY,
{
// Async transform is always ESM.
swc: swcPresets.getJestOptions({
module: 'esm',
experimental: swcExperimentalOptions,
}),
},
)
.setInjectScripts([getReactNativeInitializeCore(ROOT)])
.setFullyTransformPackages(transformer?.fullyTransformPackageNames ?? [])
.setStripFlowPackages(transformer?.stripFlowPackageNames ?? [])
.setAdditionalBabelTransformRules(
transformer?.additionalTransformRules?.babel ?? [],
)
.setAdditionalSwcTransformRules(
transformer?.additionalTransformRules?.swc ?? [],
)
.build();

return {
canInstrument: instrumentEnabled,
process: (
code: string,
path: string,
_options: TransformOptions,
): TransformedSource => {
const transformResult = syncTransformPipeline.transform(code, {
path,
pluginData: DUMMY_ESBUILD_VALUE,
namespace: DUMMY_ESBUILD_VALUE,
suffix: DUMMY_ESBUILD_VALUE,
});
return { code: transformResult.code };
},
processAsync: async (
code: string,
path: string,
_options: TransformOptions,
): Promise<TransformedSource> => {
const transformResult = await asyncTransformPipeline.transform(code, {
path,
pluginData: DUMMY_ESBUILD_VALUE,
namespace: DUMMY_ESBUILD_VALUE,
suffix: DUMMY_ESBUILD_VALUE,
});
return { code: transformResult.code };
},
getCacheKey: (
code: string,
path: string,
options: TransformOptions,
): string => {
// @ts-expect-error -- `NewGetCacheKeyFunction`
return md5(cacheKeyFunction(code, path, options));
},
};
};
1 change: 1 addition & 0 deletions packages/jest/lib/transformer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './createTransformer';
3 changes: 3 additions & 0 deletions packages/jest/lib/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { SwcJestPresetOptions } from '@react-native-esbuild/transformer';

export type TransformerConfig = Omit<SwcJestPresetOptions, 'module'>;
Loading

1 comment on commit abbda25

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🔴 Statements 14.91% 359/2408
🔴 Branches 15.52% 137/883
🔴 Functions 10.41% 69/663
🔴 Lines 14.23% 316/2221

Test suite run success

83 tests passing in 10 suites.

Report generated by 🧪jest coverage report action from abbda25

Please sign in to comment.