= (code, context) => {
13 | return stripFlowTypeofImportStatements(
14 | transform(code, {
15 | transforms: TRANSFORM_FOR_STRIP_FLOW,
16 | filePath: context.path,
17 | }).code,
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/example/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { styled, Pressable, Text } from 'dripsy';
3 |
4 | interface ButtonProps {
5 | label: string;
6 | onPress?: () => void;
7 | }
8 |
9 | const ButtonContainer = styled(Pressable)({
10 | justifyContent: 'center',
11 | alignItems: 'center',
12 | width: '100%',
13 | height: 56,
14 | borderRadius: 8,
15 | backgroundColor: '#e2e2e2',
16 | });
17 |
18 | const Label = styled(Text, { defaultVariant: 'text.button' })({
19 | fontSize: '$button',
20 | });
21 |
22 | export function Button({ label, onPress }: ButtonProps): React.JSX.Element {
23 | return (
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/.yarn/patches/dripsy-npm-4.3.8-208587309d.patch:
--------------------------------------------------------------------------------
1 | diff --git a/src/core/index.ts b/src/core/index.ts
2 | index 1419f9162a578851a0dbcba565c57f61e68a379c..d27d9130f186868a212c3d7b309836b4bced4080 100644
3 | --- a/src/core/index.ts
4 | +++ b/src/core/index.ts
5 | @@ -4,7 +4,7 @@ export * from './provider'
6 | export * from './css/scales'
7 | export { useResponsiveValue } from './css/use-responsive-value'
8 | export { Styles, css } from './css'
9 | -export { DripsyFinalTheme as Theme } from './types-v2/declarations'
10 | +export { type DripsyFinalTheme as Theme } from './types-v2/declarations'
11 | export { styled } from './css/styled'
12 | export { useBreakpointIndex } from './css/breakpoint-context'
13 | export { getBreakpointIndex } from './css/get-breakpoint-index'
14 |
--------------------------------------------------------------------------------
/docs/pages/native-setup/ios.md:
--------------------------------------------------------------------------------
1 | # iOS
2 |
3 | ## Set Custom CLI
4 |
5 | Open XCode, go to `Build Target > Build Phases > Bundle React Native code and images` and add `CLI_PATH` environment variable.
6 |
7 | ```bash
8 | set -e
9 |
10 | CLI_PATH="../node_modules/@react-native-esbuild/cli/dist/index.js"
11 |
12 | WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
13 | REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh"
14 |
15 | /bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE"
16 | ```
17 |
18 | Export if you need to build from command line or external environment(eg. Fastlane, Bitrise, etc).
19 |
20 | ```bash
21 | export CLI_PATH="$PROJECT_DIR/node_modules/@react-native-esbuild/cli/dist/index.js"
22 | ```
23 |
--------------------------------------------------------------------------------
/example/jest.config.js:
--------------------------------------------------------------------------------
1 | const TRANSFORM_PACKAGES = [
2 | 'react-native',
3 | 'jest-react-native',
4 | '@react-native',
5 | '@react-native-community',
6 | '@react-navigation',
7 | '@expo/html-elements',
8 | 'dripsy',
9 | ];
10 |
11 | /**
12 | * @type {import('jest').Config}
13 | */
14 | export default {
15 | preset: 'react-native',
16 | transform: {
17 | '^.+\\.(t|j)sx?$': '@react-native-esbuild/jest',
18 | },
19 | transformIgnorePatterns: [
20 | `node_modules/(?!${TRANSFORM_PACKAGES.join('|')})/`,
21 | ],
22 | testPathIgnorePatterns: ['dist'],
23 | coveragePathIgnorePatterns: ['node_modules'],
24 | collectCoverageFrom: [
25 | 'src/**/*.{js,jsx,ts,tsx}',
26 | '!**/*.d.ts',
27 | '!**/index.ts',
28 | ],
29 | setupFilesAfterEnv: ['./tests/setup.ts'],
30 | };
31 |
--------------------------------------------------------------------------------
/packages/transformer/types/lib.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'hermes-parser' {
2 | import type { Node } from '@babel/core';
3 |
4 | interface HermesParserOptions {
5 | babel?: boolean;
6 | allowReturnOutsideFunction?: boolean;
7 | flow?: 'all' | 'detect';
8 | sourceFilename?: string | null;
9 | sourceType?: 'module' | 'script' | 'unambiguous';
10 | tokens?: boolean;
11 | }
12 |
13 | /**
14 | * Returns `Node` when call with `babel: true` option.
15 | */
16 | type MaybeBabelAstNode = Node;
17 |
18 | interface HermesParser {
19 | parse: (code: string, options: HermesParserOptions) => MaybeBabelAstNode;
20 | }
21 |
22 | declare function parse(
23 | code: string,
24 | options: HermesParserOptions,
25 | ): MaybeBabelAstNode;
26 |
27 | export const parse;
28 | }
29 |
--------------------------------------------------------------------------------
/example/android/app/src/release/java/com/example/ReactNativeFlipper.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (c) Meta Platforms, Inc. and affiliates.
3 | *
4 | * This source code is licensed under the MIT license found in the LICENSE file in the root
5 | * directory of this source tree.
6 | */
7 | package com.example;
8 |
9 | import android.content.Context;
10 | import com.facebook.react.ReactInstanceManager;
11 |
12 | /**
13 | * Class responsible of loading Flipper inside your React Native application. This is the release
14 | * flavor of it so it's empty as we don't want to load Flipper.
15 | */
16 | public class ReactNativeFlipper {
17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
18 | // Do nothing as we don't want to initialize Flipper on Release.
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/cli/lib/helpers/cli.ts:
--------------------------------------------------------------------------------
1 | import { colors } from '@react-native-esbuild/utils';
2 | import { logger } from '../shared';
3 |
4 | export const getCommand = (
5 | argv: RawArgv,
6 | position = 0,
7 | ): string => argv._[position]?.toString() ?? '';
8 |
9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow any type for debugging.
10 | export const printDebugOptions = >(
11 | options: T,
12 | ): void => {
13 | Object.entries(options).forEach(([key, value], index, entries) => {
14 | const isLast = entries.length - 1 === index;
15 | const pipe = `${isLast ? '╰' : '├'}─`;
16 | const keyValue = `${key}: ${JSON.stringify(value)}`;
17 | logger.debug(colors.gray(`${pipe} ${keyValue}${isLast ? '\n' : ''}`));
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/packages/core/README.md:
--------------------------------------------------------------------------------
1 | # `@react-native-esbuild/core`
2 |
3 | > Core of @react-native-esbuild
4 |
5 | ## Usage
6 |
7 | ```ts
8 | import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
9 |
10 | // Must be called first
11 | ReactNativeEsbuildBundler.bootstrap();
12 |
13 | const bundler = new ReactNativeEsbuildBundler();
14 |
15 | bundler
16 | .addPlugin(/* call ReactNativeEsbuildPluginCreator */)
17 | .addPlugin(/* call ReactNativeEsbuildPluginCreator */)
18 | .addPlugin(/* call ReactNativeEsbuildPluginCreator */);
19 |
20 | // initialize bundler
21 | await bundler.initialize();
22 |
23 | // using `esbuild.build()` (write output to file system)
24 | await bundler.bundle(bundleOptions);
25 |
26 | // using `esbuild.context()` (in-memory output for dev-server)
27 | await bundler.getBundleResult(bundleOptions);
28 | ```
29 |
--------------------------------------------------------------------------------
/packages/plugins/lib/reactNativeWebPlugin/helpers/template.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import path from 'node:path';
3 | import { logger } from '../../shared';
4 |
5 | export const generateIndexPage = async (
6 | template: string,
7 | destinationDir: string,
8 | placeholders: Record = {},
9 | ): Promise => {
10 | const destination = path.join(destinationDir, 'index.html');
11 | logger.debug('generating index page', {
12 | template,
13 | destination,
14 | placeholders,
15 | });
16 |
17 | const rawTemplate = await fs.readFile(template, 'utf-8');
18 | await fs.writeFile(
19 | destination,
20 | Object.entries(placeholders).reduce((prev, [key, value]) => {
21 | return prev.replace(new RegExp(`{{${key}}}`, 'g'), value);
22 | }, rawTemplate),
23 | 'utf-8',
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/packages/cli/lib/presets/index.ts:
--------------------------------------------------------------------------------
1 | import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
2 | import {
3 | createAssetRegisterPlugin,
4 | createReactNativeRuntimeTransformPlugin,
5 | createReactNativeWebPlugin,
6 | createSvgTransformPlugin,
7 | } from '@react-native-esbuild/plugins';
8 |
9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- allow
10 | type AnyPlugin = any;
11 |
12 | const native: ReactNativeEsbuildPluginCreator[] = [
13 | createAssetRegisterPlugin,
14 | createSvgTransformPlugin,
15 | createReactNativeRuntimeTransformPlugin,
16 | ];
17 |
18 | const web: ReactNativeEsbuildPluginCreator[] = [
19 | createReactNativeWebPlugin,
20 | createSvgTransformPlugin,
21 | createReactNativeRuntimeTransformPlugin,
22 | ];
23 |
24 | export const presets = { native, web };
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Clone '...'
16 | 2. Install dependencies
17 | 3. Build and see error
18 |
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Build logs**
23 | If applicable, add logs to help explain your problem.
24 |
25 | **Environment (please complete the following information):**
26 | - OS: [e.g. macOS 13]
27 | - Node: [e.g. 8.0.0]
28 | - React Native: [e.g. 0.70.2]
29 | - React Native Esbuild: [e.g. 0.1.0]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/example/ios/exampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/example/src/navigators/RootStack.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createStackNavigator } from '@react-navigation/stack';
3 | import { IntroScreen, MainScreen } from '../screens';
4 | import type { RootStackScreens } from './types';
5 |
6 | const RootStack = createStackNavigator();
7 |
8 | export function RootStackNavigator(): React.ReactElement {
9 | return (
10 |
16 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/plugins/statusPlugin/templates.ts:
--------------------------------------------------------------------------------
1 | import { isTTY, colors } from '@react-native-esbuild/utils';
2 |
3 | const summaryTemplateForTTY = `
4 | ╭───────────╯
5 | ├─ @warnings warnings
6 | ├─ @errors errors
7 | ╰─ @duration
8 | `.trim();
9 |
10 | const summaryTemplateForNonTTY = `
11 | > @warnings warnings
12 | > @errors errors
13 | > @durations
14 | `.trim();
15 |
16 | export const getSummaryTemplate = (): string => {
17 | return colors.gray(
18 | isTTY() ? summaryTemplateForTTY : summaryTemplateForNonTTY,
19 | );
20 | };
21 |
22 | export const fromTemplate = (
23 | template: string,
24 | placeholders: Record,
25 | ): string => {
26 | const templateText = Object.entries(placeholders).reduce(
27 | (template, [key, value]) => template.replace(`@${key}`, value),
28 | template,
29 | );
30 |
31 | return `${templateText}\n`;
32 | };
33 |
--------------------------------------------------------------------------------
/docs/pages/getting-started/installation.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | Requirements: `Node>=16`
4 |
5 | ```bash
6 | # using npm
7 | npm install -D @react-native-esbuild/cli
8 |
9 | # using yarn
10 | yarn add -D @react-native-esbuild/cli
11 | ```
12 |
13 | ## Configuration
14 |
15 | Add `.rne` and `.swc` to `.gitignore`.
16 |
17 | ```sh
18 | # @react-native-esbuild's local cache directory for development environment
19 | .rne
20 |
21 | # swc plugins
22 | .swc
23 | ```
24 |
25 | And create `react-native-esbuild.config.js` to project root.
26 |
27 | ```js
28 | /**
29 | * @type {import('@react-native-esbuild/core').Config}
30 | */
31 | exports.default = {};
32 | ```
33 |
34 | for more details, go to [Configuration](/configuration/basic-configuration).
35 |
36 | ## Native Setup
37 |
38 | If you want to integrate with build processes, go to Native Setup([Android](/native-setup/android), [iOS](/native-setup/ios)) guide.
39 |
--------------------------------------------------------------------------------
/packages/core/static/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Native Esbuild
7 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/example/react-native-esbuild.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@react-native-esbuild/core').Config}
3 | */
4 | export default {
5 | cache: true,
6 | logger: {
7 | timestamp: 'YYYY-MM-DD HH:mm:ss.SSS',
8 | },
9 | transformer: {
10 | stripFlowPackageNames: ['react-native'],
11 | fullyTransformPackageNames: [],
12 | additionalTransformRules: {
13 | babel: [
14 | {
15 | test: (path, code) => {
16 | return (
17 | /node_modules\/react-native-reanimated\//.test(path) ||
18 | code.includes('react-native-reanimated')
19 | );
20 | },
21 | options: {
22 | plugins: [
23 | '@babel/plugin-transform-export-namespace-from',
24 | 'react-native-reanimated/plugin',
25 | ],
26 | babelrc: false,
27 | },
28 | },
29 | ],
30 | },
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/packages/dev-server/lib/middlewares/indexPage.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import path from 'node:path';
3 | import { logger } from '../shared';
4 | import type { DevServerMiddlewareCreator } from '../types';
5 |
6 | const TAG = 'index-page-middleware';
7 |
8 | export const createIndexPageMiddleware: DevServerMiddlewareCreator = () => {
9 | return function indexPageMiddleware(request, response, next) {
10 | if (request.url !== '/') {
11 | next();
12 | return;
13 | }
14 |
15 | fs.readFile(path.resolve(__filename, '../../static/index.html'), {
16 | encoding: 'utf-8',
17 | })
18 | .then((content) => {
19 | response.writeHead(200, { 'Content-Type': 'text/html' }).end(content);
20 | })
21 | .catch((error) => {
22 | logger.error(`(${TAG}) cannot read index.html`, error as Error);
23 | response.writeHead(500).end();
24 | });
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/example/ios/example/AppDelegate.mm:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 |
3 | #import
4 |
5 | @implementation AppDelegate
6 |
7 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
8 | {
9 | self.moduleName = @"example";
10 | // You can add your custom initial props in the dictionary below.
11 | // They will be passed down to the ViewController used by React Native.
12 | self.initialProps = @{};
13 |
14 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
15 | }
16 |
17 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
18 | return [self bundleURL];
19 | }
20 |
21 | - (NSURL *)bundleURL
22 | {
23 | #if DEBUG
24 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
25 | #else
26 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
27 | #endif
28 | }
29 |
30 | @end
31 |
--------------------------------------------------------------------------------
/packages/internal/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { createRequire } from 'node:module';
3 | import indent from 'indent-string';
4 |
5 | const require = createRequire(import.meta.url);
6 |
7 | export const resolveFromRoot = (
8 | targetPath: string,
9 | root = process.cwd(),
10 | ): string => {
11 | return require.resolve(targetPath, {
12 | paths: Array.from(
13 | new Set([
14 | // Add current directory to module resolution path for workspace projects(eg. monorepo).
15 | path.join(root, 'node_modules'),
16 | ...(require.main?.paths ?? []),
17 | ]),
18 | ).filter(Boolean),
19 | });
20 | };
21 |
22 | export const wrapWithIIFE = (body: string, filepath: string): string => `
23 | // ${filepath}
24 | (function (global) {
25 | ${indent(body, 2)}
26 | })(typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);
27 | `;
28 |
--------------------------------------------------------------------------------
/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import { transform } from '@svgr/core';
3 | import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
4 |
5 | const NAME = 'svg-transform-plugin';
6 |
7 | export const createSvgTransformPlugin: ReactNativeEsbuildPluginCreator = (
8 | context,
9 | ) => ({
10 | name: NAME,
11 | setup: (build): void => {
12 | const isNative = ['android', 'ios'].includes(context.platform);
13 |
14 | build.onLoad({ filter: /\.svg$/ }, async (args) => {
15 | const rawSvg = await fs.readFile(args.path, { encoding: 'utf8' });
16 | return {
17 | contents: await transform(
18 | rawSvg,
19 | {
20 | plugins: ['@svgr/plugin-jsx'],
21 | native: isNative,
22 | },
23 | { filePath: args.path },
24 | ),
25 | loader: 'jsx',
26 | };
27 | });
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example
2 |
3 | import com.facebook.react.ReactActivity
4 | import com.facebook.react.ReactActivityDelegate
5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
6 | import com.facebook.react.defaults.DefaultReactActivityDelegate
7 |
8 | class MainActivity : ReactActivity() {
9 |
10 | /**
11 | * Returns the name of the main component registered from JavaScript. This is used to schedule
12 | * rendering of the component.
13 | */
14 | override fun getMainComponentName(): String = "example"
15 |
16 | /**
17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
19 | */
20 | override fun createReactActivityDelegate(): ReactActivityDelegate =
21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22 | }
23 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import path from 'node:path';
3 | import type { BuildResult } from 'esbuild';
4 | import { logger } from '../../../shared';
5 | import type { ReactNativeEsbuildPluginCreator } from '../../../types';
6 |
7 | const NAME = 'metafile-plugin';
8 |
9 | export const createMetafilePlugin: ReactNativeEsbuildPluginCreator = (
10 | context,
11 | ) => ({
12 | name: NAME,
13 | setup: (build): void => {
14 | build.onEnd(async (result: BuildResult) => {
15 | const { metafile } = result;
16 | const filename = path.join(
17 | context.root,
18 | `metafile-${context.platform}-${new Date().getTime().toString()}.json`,
19 | );
20 |
21 | if (metafile) {
22 | logger.debug('writing esbuild metafile', { destination: filename });
23 | await fs.writeFile(filename, JSON.stringify(metafile), {
24 | encoding: 'utf-8',
25 | });
26 | }
27 | });
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/packages/dev-server/lib/server/DevServer.ts:
--------------------------------------------------------------------------------
1 | import type { Server as HTTPServer } from 'node:http';
2 | import type { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
3 | import { DEFAULT_HOST, DEFAULT_PORT } from '../constants';
4 | import type { DevServerOptions } from '../types';
5 |
6 | export abstract class DevServer {
7 | protected initialized = false;
8 | protected server?: HTTPServer;
9 | protected bundler?: ReactNativeEsbuildBundler;
10 | protected devServerOptions: Required;
11 |
12 | constructor(devServerOptions: DevServerOptions) {
13 | this.devServerOptions = {
14 | root: devServerOptions.root ?? process.cwd(),
15 | port: devServerOptions.port ?? DEFAULT_PORT,
16 | host: devServerOptions.host ?? DEFAULT_HOST,
17 | };
18 | }
19 |
20 | public abstract initialize(
21 | onPostSetup?: (bundler: ReactNativeEsbuildBundler) => void | Promise,
22 | ): Promise;
23 |
24 | public abstract listen(onListen?: () => void): Promise;
25 | }
26 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { SafeAreaProvider } from 'react-native-safe-area-context';
3 | import { GestureHandlerRootView } from 'react-native-gesture-handler';
4 | import {
5 | NavigationContainer,
6 | useNavigationContainerRef,
7 | } from '@react-navigation/native';
8 | import { DripsyProvider } from 'dripsy';
9 | import { RootStackNavigator } from './navigators';
10 | import { themeLight } from './theme';
11 |
12 | export function App(): React.JSX.Element {
13 | const navigationRef = useNavigationContainerRef();
14 |
15 | useEffect(() => {
16 | console.log('Hello, world!');
17 | }, []);
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/packages/dev-server/README.md:
--------------------------------------------------------------------------------
1 | # `@react-native-esbuild/dev-server`
2 |
3 | > Development server for @react-native-esbuild
4 |
5 | ## Usage
6 |
7 | ```ts
8 | import {
9 | ReactNativeAppServer,
10 | ReactNativeWebServer,
11 | } from '@react-native-esbuild/dev-server';
12 |
13 | const serveOptions = {
14 | port: '8081',
15 | host: 'localhost',
16 | };
17 |
18 | // For Native (Android, iOS)
19 | const server = await new ReactNativeAppServer(
20 | serveOptions,
21 | ).initialize((bundler) => {
22 | bundler
23 | .registerPlugin(/* plugin */)
24 | .registerPlugin(/* plugin */)
25 | .registerPlugin(/* plugin */);
26 | });
27 |
28 | // For Web
29 | const server = await new ReactNativeWebServer(
30 | serveOptions,
31 | bundleOptions,
32 | ).initialize((bundler) => {
33 | bundler
34 | .registerPlugin(createSvgTransformPlugin())
35 | .registerPlugin(createReactNativeRuntimeTransformPlugin())
36 | .registerPlugin(createReactNativeWebPlugin());
37 | });
38 |
39 | server.listen(() => console.log('listening!'));
40 | ```
41 |
--------------------------------------------------------------------------------
/packages/jest/README.md:
--------------------------------------------------------------------------------
1 | # `@react-native-esbuild/jest`
2 |
3 | > react-native preset for jest powered by @react-native-esbuild
4 |
5 | ## Usage
6 |
7 | ```js
8 | exports.default = {
9 | preset: 'react-native',
10 | transform: {
11 | '^.+\\.(t|j)sx?$': '@react-native-esbuild/jest',
12 | },
13 | };
14 |
15 | // With transformer options(for enable custom instrument).
16 | exports.default = {
17 | preset: 'react-native',
18 | transform: {
19 | '^.+\\.(t|j)sx?$': ['@react-native-esbuild/jest', /* TransformerConfig */],
20 | },
21 | };
22 | ```
23 |
24 | ```ts
25 | /**
26 | * @see {@link https://github.com/kwonoj/swc-plugin-coverage-instrument}
27 | */
28 | interface TransformerConfig {
29 | experimental?: {
30 | customCoverageInstrumentation?: {
31 | coverageVariable?: string;
32 | compact: boolean;
33 | reportLogic: boolean;
34 | ignoreClassMethods: string[];
35 | instrumentLog?: {
36 | level: string;
37 | enableTrace: boolean;
38 | };
39 | };
40 | };
41 | }
42 | ```
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Geunhyeok LEE
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/ios/example/Images.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ios-marketing",
45 | "scale" : "1x",
46 | "size" : "1024x1024"
47 | }
48 | ],
49 | "info" : {
50 | "author" : "xcode",
51 | "version" : 1
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/example/src/components/Fade.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, type PropsWithChildren } from 'react';
2 | import type { ViewStyle } from 'react-native';
3 | import Animated, {
4 | useSharedValue,
5 | useAnimatedStyle,
6 | withDelay,
7 | withTiming,
8 | interpolate,
9 | } from 'react-native-reanimated';
10 |
11 | interface FadeProps {
12 | delay?: number;
13 | style?: ViewStyle;
14 | }
15 |
16 | const TRANSLATE_OFFSET = 50;
17 |
18 | export function Fade({
19 | children,
20 | style,
21 | delay = 0,
22 | }: PropsWithChildren): React.ReactElement {
23 | const styleValue = useSharedValue(0);
24 | const animatedStyles = useAnimatedStyle(() => ({
25 | opacity: styleValue.value,
26 | transform: [
27 | {
28 | translateY: interpolate(
29 | styleValue.value,
30 | [0, 1],
31 | [TRANSLATE_OFFSET, 0],
32 | ),
33 | },
34 | ],
35 | }));
36 |
37 | useEffect(() => {
38 | styleValue.value = withDelay(delay, withTiming(1));
39 | }, [delay]);
40 |
41 | return (
42 | {children}
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/example/ios/example/PrivacyInfo.xcprivacy:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSPrivacyAccessedAPITypes
6 |
7 |
8 | NSPrivacyAccessedAPIType
9 | NSPrivacyAccessedAPICategoryFileTimestamp
10 | NSPrivacyAccessedAPITypeReasons
11 |
12 | C617.1
13 |
14 |
15 |
16 | NSPrivacyAccessedAPIType
17 | NSPrivacyAccessedAPICategoryUserDefaults
18 | NSPrivacyAccessedAPITypeReasons
19 |
20 | CA92.1
21 |
22 |
23 |
24 | NSPrivacyAccessedAPIType
25 | NSPrivacyAccessedAPICategorySystemBootTime
26 | NSPrivacyAccessedAPITypeReasons
27 |
28 | 35F9.1
29 |
30 |
31 |
32 | NSPrivacyCollectedDataTypes
33 |
34 | NSPrivacyTracking
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/docs/pages/troubleshooting/basic.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | ## On build time
4 |
5 | - When `No matching export in "xxx" for import "xxx"` occurs
6 | - Replace import statements to `type import` statements.
7 | ```ts
8 | // AS IS
9 | import { ValueIdentifier, TypeIdentifierA } from '...';
10 | import TypeIdentifierB from '...';
11 |
12 | // TO BE
13 | import { ValueIdentifier, type TypeIdentifierA } from '...';
14 | import type TypeIdentifierB from '...';
15 | ```
16 | - When `Syntax Error` occurs
17 | - Target library may be using [Flow](https://flow.org).
18 | - Add the package name to `stripFlowPackageNames` in configuration file.
19 | - When unknown error occurs
20 | - Add the package name to `fullyTransformPackageNames` in configuration file.
21 | - Try rebuild with `--reset-cache` flag.
22 | - Please [report issue](https://github.com/leegeunhyeok/react-native-esbuild/issues) and share demo code for reproduce the issue.
23 |
24 | ## On runtime
25 |
26 | There're many reasons for runtime errors, making it difficult to determine the cause.
27 |
28 | Please [report issue](https://github.com/leegeunhyeok/react-native-esbuild/issues) and share demo code for reproduce the issue.
29 |
--------------------------------------------------------------------------------
/example/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 |
3 | const project = resolve(__dirname, 'tsconfig.json');
4 |
5 | /** @type { import('eslint').ESLint.ConfigData } */
6 | module.exports = {
7 | root: true,
8 | env: {
9 | node: true,
10 | },
11 | plugins: ['prettier'],
12 | extends: [
13 | require.resolve('@vercel/style-guide/eslint/node'),
14 | require.resolve('@vercel/style-guide/eslint/typescript'),
15 | ],
16 | parserOptions: {
17 | project,
18 | },
19 | settings: {
20 | 'import/resolver': {
21 | typescript: {
22 | project,
23 | },
24 | },
25 | },
26 | overrides: [
27 | {
28 | files: ['*.ts?(x)', '*.js?(x)'],
29 | rules: {
30 | semi: ['error', 'always'],
31 | quotes: ['error', 'single'],
32 | 'object-curly-spacing': ['error', 'always'],
33 | 'array-bracket-spacing': 'off',
34 | 'no-console': 'off',
35 | 'unicorn/filename-case': 'off',
36 | 'prettier/prettier': 'error',
37 | },
38 | },
39 | {
40 | files: ['*.test.ts?(x)', '*.spec.js?(x)'],
41 | rules: {
42 | 'import/no-named-as-default-member': 'off',
43 | },
44 | },
45 | ],
46 | };
47 |
--------------------------------------------------------------------------------
/packages/dev-server/types/lib.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'metro-inspector-proxy' {
2 | import type { WebSocketServer } from 'ws';
3 |
4 | class InspectorProxy {
5 | constructor(projectRoot: string);
6 |
7 | /**
8 | * Process HTTP request sent to server. We only respond to 2 HTTP requests:
9 | * 1. /json/version returns Chrome debugger protocol version that we use
10 | * 2. /json and /json/list returns list of page descriptions (list of inspectable apps).
11 | * This list is combined from all the connected devices.
12 | */
13 | processRequest: (
14 | request: IncomingMessage,
15 | response: ServerResponse,
16 | next: (error?: Error) => void,
17 | ) => void;
18 |
19 | /**
20 | * Adds websocket listeners to the provided HTTP/HTTPS server.
21 | */
22 | createWebSocketListeners: (server: HTTPServer) => {
23 | ['/inspector/device']: WebSocketServer;
24 | ['/inspector/debug']: WebSocketServer;
25 | };
26 | }
27 |
28 | export { InspectorProxy };
29 | }
30 |
31 | declare module 'http' {
32 | interface IncomingMessage {
33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow any.
34 | rawBody: any;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Modules */
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "moduleDetection": "force",
7 | "resolveJsonModule": true,
8 | "isolatedModules": true,
9 |
10 | /* Type Checking */
11 | "types": ["node", "jest"],
12 | "noFallthroughCasesInSwitch": true,
13 | "noImplicitReturns": true,
14 | "noUncheckedIndexedAccess": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "strict": true,
18 |
19 | /* Language and Environment */
20 | "experimentalDecorators": true,
21 | "jsx": "react-jsx",
22 | "lib": ["ESNext", "ESNext.AsyncIterable"],
23 | "target": "ESNext",
24 |
25 | /* Interop Constraints */
26 | "esModuleInterop": true,
27 | "forceConsistentCasingInFileNames": true,
28 |
29 | /* Completeness */
30 | "allowJs": false,
31 | "skipLibCheck": true,
32 |
33 | /* Others */
34 | "stripInternal": true
35 | },
36 | "files": ["types/index.d.ts"],
37 | "include": [
38 | "packages/**/*",
39 | "example/**/*",
40 | "jest.config.ts",
41 | "test/setup.ts"
42 | ],
43 | "exclude": ["**/*/node_modules", "**/dist/*", "**/esm/*"]
44 | }
45 |
--------------------------------------------------------------------------------
/packages/config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/config",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "shared configs for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "main": "dist/index.js",
14 | "types": "dist/lib/index.d.ts",
15 | "scripts": {
16 | "prepack": "yarn cleanup && yarn build",
17 | "typecheck": "tsc --noEmit",
18 | "cleanup": "rimraf ./dist",
19 | "build": "node build/index.js && tsc"
20 | },
21 | "directories": {
22 | "lib": "lib",
23 | "test": "__tests__"
24 | },
25 | "files": [
26 | "lib",
27 | "dist"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
35 | },
36 | "bugs": {
37 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
38 | },
39 | "devDependencies": {
40 | "esbuild": "^0.25.5",
41 | "typescript": "^5.8.3"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/logo.ts:
--------------------------------------------------------------------------------
1 | import { colors, isTTY } from '@react-native-esbuild/utils';
2 |
3 | const LOGO = `
4 | "88e "88e
5 | "88e "88e
6 | "88e "88e
7 | e88" e88"
8 | e88" e88"
9 | e88" e88"
10 | `;
11 |
12 | // Center column index of `LOGO`
13 | const LOGO_CENTER_X = 18;
14 |
15 | export const ESBUILD_LABEL = ' » esbuild ';
16 | const DESCRIPTION = 'An extremely fast bundler';
17 |
18 | export const printLogo = (): void => {
19 | if (isTTY()) {
20 | process.stdout.write(`${colors.yellow(LOGO)}\n`);
21 | process.stdout.write(
22 | [
23 | colors.bgYellow(colors.black(ESBUILD_LABEL)),
24 | colors.gray(DESCRIPTION),
25 | '\n',
26 | ].join(' '),
27 | );
28 | } else {
29 | process.stdout.write(`${ESBUILD_LABEL.trim()} - ${DESCRIPTION}\n`);
30 | }
31 | };
32 |
33 | export const printVersion = (): void => {
34 | const paddingForCenterAlign = new Array(
35 | Math.floor(LOGO_CENTER_X - (self._version as string).length / 2),
36 | )
37 | .fill(' ')
38 | .join('');
39 | process.stdout.write(
40 | `${isTTY() ? paddingForCenterAlign : ''}v${self._version}\n\n`,
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/cli/lib/commands/serve.ts:
--------------------------------------------------------------------------------
1 | import { ReactNativeWebServer } from '@react-native-esbuild/dev-server';
2 | import type { BundleOptions } from '@react-native-esbuild/config';
3 | import { printDebugOptions } from '../helpers';
4 | import { serveArgvSchema } from '../schema';
5 | import { presets } from '../presets';
6 | import { logger } from '../shared';
7 | import type { Command } from '../types';
8 |
9 | /**
10 | * Start dev server for web.
11 | */
12 | export const serve: Command = async (argv): Promise => {
13 | const serveArgv = serveArgvSchema.parse(argv);
14 | const serveOptions = {
15 | host: serveArgv.host,
16 | port: serveArgv.port,
17 | };
18 |
19 | const bundleOptions: Partial = {
20 | platform: 'web',
21 | entry: serveArgv['entry-file'],
22 | dev: serveArgv.dev,
23 | minify: serveArgv.minify,
24 | };
25 |
26 | logger.debug('serve options');
27 | printDebugOptions(serveOptions);
28 | logger.debug('bundle options');
29 | printDebugOptions(bundleOptions);
30 |
31 | const server = await new ReactNativeWebServer(
32 | serveOptions,
33 | bundleOptions,
34 | ).initialize((bundler) => {
35 | presets.web.forEach(bundler.addPlugin.bind(bundler));
36 | });
37 |
38 | await server.listen();
39 | };
40 |
--------------------------------------------------------------------------------
/packages/cli/lib/helpers/stdin.ts:
--------------------------------------------------------------------------------
1 | import readline from 'node:readline';
2 | import { logger } from '../shared';
3 |
4 | export const enableInteractiveMode = (
5 | onKeypress?: (keyName: string) => void,
6 | ): boolean => {
7 | if (
8 | !(process.stdin.isTTY && typeof process.stdin.setRawMode === 'function')
9 | ) {
10 | logger.debug('interactive mode is not supported in this environment');
11 | return false;
12 | }
13 |
14 | /**
15 | * @see {@link https://nodejs.org/api/tty.html#readstreamsetrawmodemode}
16 | */
17 | readline.emitKeypressEvents(process.stdin);
18 | process.stdin.setRawMode(true);
19 | process.stdin.setEncoding('utf8');
20 | process.stdin.on(
21 | 'keypress',
22 | (_data, key: { ctrl: boolean; name: string }) => {
23 | const { ctrl, name } = key;
24 |
25 | // Handle shortcuts.
26 | if (ctrl) {
27 | switch (name) {
28 | // Ctrl + C: SIGINT
29 | case 'c':
30 | process.exit(0);
31 | break;
32 |
33 | // Ctrl + Z: SIGTSTP
34 | case 'z':
35 | process.emit('SIGTSTP', 'SIGTSTP');
36 | break;
37 | }
38 | return;
39 | }
40 |
41 | onKeypress?.(name);
42 | },
43 | );
44 |
45 | return true;
46 | };
47 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/cache/CacheController.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import type { Cache } from '../../types';
4 |
5 | const OPTIONS = { encoding: 'utf-8' } as const;
6 |
7 | export class CacheController {
8 | private cache: Record = {};
9 |
10 | constructor(private cacheDirectory: string) {
11 | try {
12 | fs.accessSync(cacheDirectory, fs.constants.R_OK | fs.constants.W_OK);
13 | } catch (_error) {
14 | fs.mkdirSync(cacheDirectory, { recursive: true });
15 | }
16 | }
17 |
18 | public readFromMemory(key: string): Cache | undefined {
19 | return this.cache[key];
20 | }
21 |
22 | public readFromFileSystem(hash: string): Promise {
23 | return fs.promises
24 | .readFile(path.join(this.cacheDirectory, hash), OPTIONS)
25 | .catch(() => null);
26 | }
27 |
28 | public writeToMemory(key: string, cacheData: Cache): void {
29 | this.cache[key] = cacheData;
30 | }
31 |
32 | public writeToFileSystem(hash: string, data: string): Promise {
33 | return fs.promises
34 | .writeFile(path.join(this.cacheDirectory, hash), data, OPTIONS)
35 | .catch(() => void 0);
36 | }
37 |
38 | public reset(): void {
39 | this.cache = {};
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Resolve react_native_pods.rb with node to allow for hoisting
2 | require Pod::Executable.execute_command('node', ['-p',
3 | 'require.resolve(
4 | "react-native/scripts/react_native_pods.rb",
5 | {paths: [process.argv[1]]},
6 | )', __dir__]).strip
7 |
8 | platform :ios, min_ios_version_supported
9 | prepare_react_native_project!
10 |
11 | linkage = ENV['USE_FRAMEWORKS']
12 | if linkage != nil
13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
14 | use_frameworks! :linkage => linkage.to_sym
15 | end
16 |
17 | target 'example' do
18 | config = use_native_modules!
19 |
20 | use_react_native!(
21 | :path => config[:reactNativePath],
22 | # An absolute path to your application root.
23 | :app_path => "#{Pod::Config.instance.installation_root}/.."
24 | )
25 |
26 | target 'exampleTests' do
27 | inherit! :complete
28 | # Pods for testing
29 | end
30 |
31 | post_install do |installer|
32 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
33 | react_native_post_install(
34 | installer,
35 | config[:reactNativePath],
36 | :mac_catalyst_enabled => false,
37 | # :ccache_enabled => true
38 | )
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/packages/jest/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | All notable changes to this project will be documented in this file.
4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5 |
6 | ## [0.1.0-beta.12](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.11...v0.1.0-beta.12) (2023-10-25)
7 |
8 | **Note:** Version bump only for package @react-native-esbuild/jest
9 |
10 | ## [0.1.0-beta.11](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.10...v0.1.0-beta.11) (2023-10-25)
11 |
12 | **Note:** Version bump only for package @react-native-esbuild/jest
13 |
14 | ## [0.1.0-beta.10](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.9...v0.1.0-beta.10) (2023-10-24)
15 |
16 | ### Features
17 |
18 | - **jest:** add @react-native-esbuild/jest ([53b1874](https://github.com/leegeunhyeok/react-native-esbuild/commit/53b1874fbdaa639b4f21ac0394285317075288c5))
19 |
20 | ### Miscellaneous Chores
21 |
22 | - fix dependency versions ([f401dc8](https://github.com/leegeunhyeok/react-native-esbuild/commit/f401dc866ec231d91cd08b03b346837c150fc272))
23 |
24 | ### Code Refactoring
25 |
26 | - function based transformer presets ([fb56af9](https://github.com/leegeunhyeok/react-native-esbuild/commit/fb56af93c9a014be97b53965001d7b62a1e74749))
27 |
--------------------------------------------------------------------------------
/packages/cli/scripts/xcode.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # https://github.com/facebook/react-native/blob/v0.72.3/packages/react-native/scripts/react-native-xcode.sh
4 |
5 | case "$CONFIGURATION" in
6 | *Debug*)
7 | if [[ "$PLATFORM_NAME" == *simulator ]]; then
8 | if [[ "$FORCE_BUNDLING" ]]; then
9 | echo "FORCE_BUNDLING enabled; continuing to bundle."
10 | else
11 | echo "Skipping bundling in Debug for the Simulator (since the packager bundles for you). Use the FORCE_BUNDLING flag to change this behavior."
12 | exit 0;
13 | fi
14 | else
15 | echo "Bundling for physical device. Use the SKIP_BUNDLING flag to change this behavior."
16 | fi
17 |
18 | DEV=true
19 | ;;
20 | "")
21 | echo "$0 must be invoked by Xcode"
22 | exit 1
23 | ;;
24 | *)
25 | DEV=false
26 | ;;
27 | esac
28 |
29 | DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH
30 | CLI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
31 | ENTRY_FILE="$(cd "$CLI_DIR/../../.." && pwd)/index.js"
32 | BUNDLE_FILE="$DEST/main.jsbundle"
33 |
34 | node "$CLI_DIR/dist/index.js" bundle \
35 | --entry-file=$ENTRY_FILE \
36 | --bundle-output=$BUNDLE_FILE \
37 | --assets-dest=$DEST \
38 | --dev=$DEV \
39 | --platform=ios \
40 | --reset-cache \
41 | --verbose
42 |
--------------------------------------------------------------------------------
/docs/pages/jest/transformer.md:
--------------------------------------------------------------------------------
1 | # Testing with Jest
2 |
3 | `react-native-esbuild/jest` is a [Jest](https://jestjs.io) transformer.
4 |
5 | It can replace [babel-jest](https://github.com/facebook/react-native/blob/v0.72.6/packages/react-native/jest-preset.js#L18) and supports the React Native testing environment.
6 |
7 | ## Setup
8 |
9 | Requirements: `Jest>=27`
10 |
11 | ```bash
12 | # using npm
13 | npm install -D @react-native-esbuild/jest
14 |
15 | # using yarn
16 | yarn add -D @react-native-esbuild/jest
17 | ```
18 |
19 | Open your `jest.config.js` and set `@react-native-esbuild/jest` as transformer.
20 |
21 | ```js
22 | // Many react-native npm modules unfortunately don't pre-compile their source code before publishing.
23 | // If you have npm dependencies that have to be transformed you can add the package name to list.
24 | const TRANSFORM_PACKAGES = [
25 | 'react-native',
26 | 'jest-react-native',
27 | '@react-native',
28 | '@react-native-community',
29 | '@react-navigation',
30 | // ...
31 | ];
32 |
33 | /**
34 | * @type {import('jest').Config}
35 | */
36 | module.exports = {
37 | preset: 'react-native',
38 | transform: {
39 | '^.+\\.(t|j)sx?$': '@react-native-esbuild/jest',
40 | },
41 | transformIgnorePatterns: [
42 | `node_modules/(?!${TRANSFORM_PACKAGES.join('|')})/`,
43 | ],
44 | };
45 | ```
46 |
--------------------------------------------------------------------------------
/packages/internal/lib/defaults.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @see {@link https://github.com/facebook/metro/blob/0.72.x/docs/Configuration.md#resolvermainfields}
3 | */
4 | export const RESOLVER_MAIN_FIELDS = [
5 | 'react-native',
6 | 'browser',
7 | 'main',
8 | 'module',
9 | ];
10 |
11 | /**
12 | * @see {@link https://github.com/facebook/metro/blob/0.72.x/packages/metro-config/src/defaults/defaults.js}
13 | * @see {@link https://github.com/facebook/metro/blob/0.72.x/packages/metro-file-map/src/workerExclusionList.js}
14 | */
15 | export const SOURCE_EXTENSIONS = [
16 | '.tsx',
17 | '.ts',
18 | '.jsx',
19 | '.js',
20 | '.mjs',
21 | '.cjs',
22 | '.json',
23 | ];
24 |
25 | export const IMAGE_EXTENSIONS = [
26 | '.bmp',
27 | '.gif',
28 | '.ico',
29 | '.jpeg',
30 | '.jpg',
31 | '.png',
32 | '.tiff',
33 | '.tif',
34 | '.webp',
35 | ];
36 |
37 | export const ASSET_EXTENSIONS = [
38 | // Video extensions.
39 | '.avi',
40 | '.mp4',
41 | '.mpeg',
42 | '.mpg',
43 | '.ogv',
44 | '.webm',
45 | '.3gp',
46 | '.3g2',
47 |
48 | // Audio extensions.
49 | '.aac',
50 | '.midi',
51 | '.mid',
52 | '.mp3',
53 | '.oga',
54 | '.wav',
55 | '.3gp',
56 | '.3g2',
57 |
58 | // Font extensions.
59 | '.eot',
60 | '.otf',
61 | '.ttf',
62 | '.woff',
63 | '.woff2',
64 |
65 | // Image extensions.
66 | ...IMAGE_EXTENSIONS,
67 | ];
68 |
--------------------------------------------------------------------------------
/packages/transformer/lib/transformer/swc/swc.ts:
--------------------------------------------------------------------------------
1 | import {
2 | transform,
3 | transformSync,
4 | minify,
5 | type Options,
6 | type JsMinifyOptions,
7 | } from '@swc/core';
8 | import type { AsyncTransformer, SyncTransformer } from '../../types';
9 |
10 | export const transformWithSwc: AsyncTransformer = async (
11 | code,
12 | context,
13 | preset,
14 | ) => {
15 | const { code: transformedCode } = await transform(code, preset?.(context));
16 |
17 | if (typeof transformedCode !== 'string') {
18 | throw new Error('swc transformed source is empty');
19 | }
20 |
21 | return transformedCode;
22 | };
23 |
24 | export const transformSyncWithSwc: SyncTransformer = (
25 | code,
26 | context,
27 | preset,
28 | ) => {
29 | const { code: transformedCode } = transformSync(code, preset?.(context));
30 |
31 | if (typeof transformedCode !== 'string') {
32 | throw new Error('swc transformed source is empty');
33 | }
34 |
35 | return transformedCode;
36 | };
37 |
38 | export const minifyWithSwc: AsyncTransformer = async (
39 | code,
40 | context,
41 | preset,
42 | ) => {
43 | const { code: minifiedCode } = await minify(code, preset?.(context));
44 |
45 | if (typeof minifiedCode !== 'string') {
46 | throw new Error('swc minified source is empty');
47 | }
48 |
49 | return minifiedCode;
50 | };
51 |
--------------------------------------------------------------------------------
/packages/internal/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/internal",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "shared configs and helpers for internal of react-native",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "main": "dist/index.js",
14 | "types": "dist/lib/index.d.ts",
15 | "scripts": {
16 | "prepack": "yarn cleanup && yarn build",
17 | "cleanup": "rimraf ./dist",
18 | "typecheck": "tsc --noEmit",
19 | "build": "node build/index.js && tsc"
20 | },
21 | "directories": {
22 | "lib": "lib",
23 | "test": "__tests__"
24 | },
25 | "files": [
26 | "lib",
27 | "dist"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
35 | },
36 | "bugs": {
37 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
38 | },
39 | "devDependencies": {
40 | "@react-native-esbuild/config": "workspace:*",
41 | "esbuild": "^0.25.5",
42 | "typescript": "^5.8.3"
43 | },
44 | "dependencies": {
45 | "indent-string": "^4.0.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/utils",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "utilities for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "main": "dist/index.js",
14 | "types": "dist/lib/index.d.ts",
15 | "scripts": {
16 | "prepack": "yarn cleanup && yarn build",
17 | "cleanup": "rimraf ./dist",
18 | "typecheck": "tsc --noEmit",
19 | "build": "node build/index.js && tsc"
20 | },
21 | "directories": {
22 | "lib": "lib",
23 | "test": "__tests__"
24 | },
25 | "files": [
26 | "lib",
27 | "dist"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
35 | },
36 | "bugs": {
37 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
38 | },
39 | "devDependencies": {
40 | "@faker-js/faker": "^8.1.0",
41 | "esbuild": "^0.25.5",
42 | "typescript": "^5.8.3"
43 | },
44 | "dependencies": {
45 | "colors": "^1.4.0",
46 | "dayjs": "^1.11.10",
47 | "node-self": "^1.0.2"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/storages/SharedStorage.ts:
--------------------------------------------------------------------------------
1 | import type { BundlerSharedData } from '../../types';
2 | import { Storage } from './Storage';
3 |
4 | export class SharedStorage extends Storage {
5 | private getDefaultSharedData(): BundlerSharedData {
6 | return {
7 | watcher: {
8 | changed: null,
9 | stats: undefined,
10 | },
11 | };
12 | }
13 |
14 | public get(key: number): BundlerSharedData {
15 | if (this.data.has(key)) {
16 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Already `has()` checked.
17 | return this.data.get(key)!;
18 | }
19 |
20 | const sharedData = this.getDefaultSharedData();
21 | this.data.set(key, sharedData);
22 |
23 | return sharedData;
24 | }
25 |
26 | public setValue(value: Partial): void {
27 | for (const sharedData of this.data.values()) {
28 | sharedData.watcher.changed =
29 | value.watcher?.changed ?? sharedData.watcher.changed;
30 | sharedData.watcher.stats =
31 | value.watcher?.stats ?? sharedData.watcher.stats;
32 | }
33 | }
34 |
35 | public clearAll(): Promise {
36 | for (const sharedData of this.data.values()) {
37 | sharedData.watcher.changed = null;
38 | sharedData.watcher.stats = undefined;
39 | }
40 | return Promise.resolve();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/caches.ts:
--------------------------------------------------------------------------------
1 | import type { CacheController } from '@react-native-esbuild/core';
2 | import type { CacheConfig } from '../../types';
3 |
4 | export const getTransformedCodeFromInMemoryCache = (
5 | controller: CacheController,
6 | cacheConfig: CacheConfig,
7 | ): string | null => {
8 | const inMemoryCache = controller.readFromMemory(cacheConfig.hash);
9 | // If file is not modified, use cache data instead.
10 | return inMemoryCache && inMemoryCache.modifiedAt === cacheConfig.mtimeMs
11 | ? inMemoryCache.data
12 | : null;
13 | };
14 |
15 | export const getTransformedCodeFromFileSystemCache = async (
16 | controller: CacheController,
17 | cacheConfig: CacheConfig,
18 | ): Promise => {
19 | const fsCache = await controller.readFromFileSystem(cacheConfig.hash);
20 | return fsCache ?? null;
21 | };
22 |
23 | export const writeTransformedCodeToInMemoryCache = (
24 | controller: CacheController,
25 | code: string,
26 | cacheConfig: CacheConfig,
27 | ): void => {
28 | controller.writeToMemory(cacheConfig.hash, {
29 | data: code,
30 | modifiedAt: cacheConfig.mtimeMs,
31 | });
32 | };
33 |
34 | export const writeTransformedCodeToFileSystemCache = (
35 | controller: CacheController,
36 | code: string,
37 | cacheConfig: CacheConfig,
38 | ): Promise => {
39 | return controller.writeToFileSystem(cacheConfig.hash, code);
40 | };
41 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/helpers/__tests__/config.test.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { loadConfig, getConfigFromGlobal } from '../config';
3 | import type { Config } from '../../../types';
4 |
5 | describe('config helpers', () => {
6 | describe('loadConfig', () => {
7 | beforeEach(() => {
8 | self._config = undefined;
9 | });
10 |
11 | afterAll(() => {
12 | delete self._config;
13 | });
14 |
15 | describe('when call loadConfig', () => {
16 | beforeEach(() => {
17 | loadConfig(faker.system.filePath());
18 | });
19 |
20 | it('should caching config data to global', () => {
21 | expect(self._config).not.toBeUndefined();
22 | });
23 | });
24 | });
25 |
26 | describe('getConfigFromGlobal', () => {
27 | beforeEach(() => {
28 | delete self._config;
29 | });
30 |
31 | describe('when call before load', () => {
32 | it('should throw error', () => {
33 | expect(() => getConfigFromGlobal()).toThrowError();
34 | });
35 | });
36 |
37 | describe('when call after load', () => {
38 | let config: Config;
39 |
40 | beforeEach(() => {
41 | config = loadConfig(faker.system.filePath());
42 | });
43 |
44 | it('should return cached config object', () => {
45 | expect(getConfigFromGlobal()).toEqual(expect.objectContaining(config));
46 | });
47 | });
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/storages/CacheStorage.ts:
--------------------------------------------------------------------------------
1 | import os from 'node:os';
2 | import fs from 'node:fs';
3 | import path from 'node:path';
4 | import { GLOBAL_CACHE_DIR } from '@react-native-esbuild/config';
5 | import { logger } from '../../shared';
6 | import { CacheController } from '../cache';
7 | import { Storage } from './Storage';
8 |
9 | const CACHE_DIRECTORY = path.join(os.tmpdir(), GLOBAL_CACHE_DIR);
10 |
11 | export class CacheStorage extends Storage {
12 | constructor() {
13 | super();
14 | try {
15 | fs.accessSync(CACHE_DIRECTORY, fs.constants.R_OK | fs.constants.W_OK);
16 | } catch (_error) {
17 | logger.debug('cache directory is not exist or no access permission');
18 | fs.mkdirSync(CACHE_DIRECTORY, { recursive: true });
19 | }
20 | }
21 |
22 | public get(key: number): CacheController {
23 | if (this.data.has(key)) {
24 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Already `has()` checked.
25 | return this.data.get(key)!;
26 | }
27 |
28 | const controller = new CacheController(CACHE_DIRECTORY);
29 | this.data.set(key, controller);
30 |
31 | return controller;
32 | }
33 |
34 | public clearAll(): Promise {
35 | for (const controller of this.data.values()) {
36 | controller.reset();
37 | }
38 | return fs.promises.rm(CACHE_DIRECTORY, {
39 | recursive: true,
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/packages/symbolicate/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/symbolicate",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "symbolicate utilities for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild",
9 | "sourcemap",
10 | "symbolicate"
11 | ],
12 | "author": "leegeunhyeok ",
13 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
14 | "license": "MIT",
15 | "main": "dist/index.js",
16 | "types": "dist/lib/index.d.ts",
17 | "scripts": {
18 | "prepack": "yarn cleanup && yarn build",
19 | "cleanup": "rimraf ./dist",
20 | "typecheck": "tsc --noEmit",
21 | "build": "node build/index.js && tsc"
22 | },
23 | "directories": {
24 | "lib": "lib",
25 | "test": "__tests__"
26 | },
27 | "files": [
28 | "lib",
29 | "dist"
30 | ],
31 | "publishConfig": {
32 | "access": "public"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
37 | },
38 | "bugs": {
39 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
40 | },
41 | "devDependencies": {
42 | "@types/babel__code-frame": "^7.0.6",
43 | "esbuild": "^0.25.5",
44 | "typescript": "^5.8.3"
45 | },
46 | "dependencies": {
47 | "@babel/code-frame": "^7.27.1",
48 | "source-map": "^0.7.4"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/packages/transformer/lib/pipelines/pipeline.ts:
--------------------------------------------------------------------------------
1 | import type { OnLoadArgs } from 'esbuild';
2 | import md5 from 'md5';
3 | import type { TransformStep, TransformerContext } from '../types';
4 |
5 | export abstract class TransformPipeline> {
6 | protected steps: Step[] = [];
7 | protected onBeforeTransform?: Step;
8 | protected onAfterTransform?: Step;
9 |
10 | constructor(protected context: Omit) {}
11 |
12 | /**
13 | * Generate hash that contains the file path, modification time, and bundle options.
14 | *
15 | * `id` is combined(platform, dev, minify) bundle options value in `@react-native-esbuild`.
16 | *
17 | * hash = md5(id + modified time + file path + core version)
18 | * number + number + string + string
19 | */
20 | protected getHash(id: number, filepath: string, modifiedAt: number): string {
21 | return md5(id + modifiedAt + filepath + self._version);
22 | }
23 |
24 | public beforeTransform(onBeforeTransform: Step): this {
25 | this.onBeforeTransform = onBeforeTransform;
26 | return this;
27 | }
28 |
29 | public afterTransform(onAfterTransform: Step): this {
30 | this.onAfterTransform = onAfterTransform;
31 | return this;
32 | }
33 |
34 | public addStep(step: Step): this {
35 | this.steps.push(step);
36 | return this;
37 | }
38 |
39 | abstract transform(code: string, args: OnLoadArgs): ReturnType;
40 | }
41 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 | **/.xcode.env.local
24 |
25 | # Android/IntelliJ
26 | #
27 | build/
28 | .idea
29 | .gradle
30 | local.properties
31 | *.iml
32 | *.hprof
33 | .cxx/
34 | *.keystore
35 | !debug.keystore
36 |
37 | # node.js
38 | #
39 | node_modules/
40 | npm-debug.log
41 | yarn-error.log
42 |
43 | # fastlane
44 | #
45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
46 | # screenshots whenever they are needed.
47 | # For more information about the recommended setup visit:
48 | # https://docs.fastlane.tools/best-practices/source-control/
49 |
50 | **/fastlane/report.xml
51 | **/fastlane/Preview.html
52 | **/fastlane/screenshots
53 | **/fastlane/test_output
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 |
58 | # Ruby / CocoaPods
59 | **/Pods/
60 | /vendor/bundle/
61 |
62 | # Temporary files created by Metro to check the health of the file watcher
63 | .metro-health-check*
64 |
65 | # testing
66 | /coverage
67 |
68 | # yarn berry
69 | .yarn/*
70 | !.yarn/patches
71 | !.yarn/plugins
72 | !.yarn/releases
73 | !.yarn/sdks
74 | !.yarn/versions
75 |
76 | # @react-native-esbuild
77 | .rne
78 | .swc
79 |
80 | # @react-native-esbuild for web
81 | public/
82 |
--------------------------------------------------------------------------------
/packages/cli/lib/commands/bundle.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
3 | import {
4 | DEFAULT_ENTRY_POINT,
5 | type BundleOptions,
6 | } from '@react-native-esbuild/config';
7 | import { printDebugOptions } from '../helpers';
8 | import { bundleArgvSchema } from '../schema';
9 | import { presets } from '../presets';
10 | import { logger } from '../shared';
11 | import type { Command } from '../types';
12 |
13 | /**
14 | * Create bundle command.
15 | */
16 | export const bundle: Command = async (argv) => {
17 | const bundleArgv = bundleArgvSchema.parse(argv);
18 | const bundleOptions: Partial = {
19 | entry: path.resolve(bundleArgv['entry-file'] ?? DEFAULT_ENTRY_POINT),
20 | sourcemap: bundleArgv['sourcemap-output'],
21 | outfile: bundleArgv['bundle-output'],
22 | assetsDir: bundleArgv['assets-dest'],
23 | platform: bundleArgv.platform,
24 | dev: bundleArgv.dev,
25 | minify: bundleArgv.minify,
26 | metafile: bundleArgv.metafile,
27 | };
28 | const root = bundleOptions.entry
29 | ? path.dirname(bundleOptions.entry)
30 | : process.cwd();
31 | logger.debug('bundle options');
32 | printDebugOptions(bundleOptions);
33 |
34 | const bundler = new ReactNativeEsbuildBundler(root);
35 | (bundleOptions.platform === 'web' ? presets.web : presets.native).forEach(
36 | bundler.addPlugin.bind(bundler),
37 | );
38 |
39 | await bundler.initialize();
40 | await bundler.bundle(bundleOptions);
41 | };
42 |
--------------------------------------------------------------------------------
/docs/theme.config.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { DocsThemeConfig, useConfig } from 'nextra-theme-docs'
3 | import { useRouter } from 'next/router'
4 | import Image from 'next/image'
5 |
6 | const config: DocsThemeConfig = {
7 | logo: (
8 | <>
9 |
16 | React Native Esbuild
17 | >
18 | ),
19 | sidebar: {
20 | defaultMenuCollapseLevel: 1,
21 | },
22 | project: {
23 | link: 'https://github.com/leegeunhyeok/react-native-esbuild',
24 | },
25 | docsRepositoryBase: 'https://github.com/leegeunhyeok/react-native-esbuild',
26 | footer: {
27 | text: 'React Native Esbuild',
28 | },
29 | head: () => {
30 | const { asPath, defaultLocale, locale } = useRouter()
31 | const { frontMatter, title } = useConfig()
32 | const url =
33 | 'https://react-native-esbuild.vercel.app' +
34 | (defaultLocale === locale ? asPath : `/${locale}${asPath}`)
35 |
36 | return (
37 | <>
38 |
39 |
40 |
41 |
45 | {`${title} - RNE`}
46 | >
47 | )
48 | }
49 | }
50 |
51 | export default config
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-esbuild",
3 | "private": true,
4 | "workspaces": [
5 | "docs",
6 | "example",
7 | "packages/*"
8 | ],
9 | "installConfig": {
10 | "hoistingLimits": "workspaces"
11 | },
12 | "scripts": {
13 | "prepare": "husky install",
14 | "build:all": "nx run-many --target=build",
15 | "typecheck:all": "nx run-many --target=typecheck",
16 | "test": "jest",
17 | "test:coverage": "jest --coverage --json --outputFile=coverage/report.json",
18 | "lint": "eslint packages",
19 | "example": "yarn --cwd example",
20 | "release": "nx release",
21 | "clean": "rimraf --glob \"packages/**/*/dist\" .nx"
22 | },
23 | "devDependencies": {
24 | "@faker-js/faker": "^9.8.0",
25 | "@nx/js": "^21.1.2",
26 | "@swc/core": "^1.11.29",
27 | "@swc/jest": "^0.2.38",
28 | "@types/invariant": "^2.2.37",
29 | "@types/jest": "^29.5.14",
30 | "@types/node": "^22",
31 | "@vercel/style-guide": "^5",
32 | "esbuild": "^0.25.5",
33 | "eslint": "^8.49.0",
34 | "eslint-plugin-prettier": "^5.4.1",
35 | "husky": "^9.1.7",
36 | "jest": "^29.7.0",
37 | "node-self": "^1.0.2",
38 | "nx": "^21.1.2",
39 | "prettier": "^3.5.3",
40 | "react-native": "0.72.6",
41 | "rimraf": "^6.0.1",
42 | "ts-node": "^10.9.2",
43 | "typescript": "^5.8.3"
44 | },
45 | "prettier": "@vercel/style-guide/prettier",
46 | "packageManager": "yarn@4.9.1",
47 | "resolutions": {
48 | "dripsy@^4.3.8": "patch:dripsy@npm%3A4.3.8#./.yarn/patches/dripsy-npm-4.3.8-208587309d.patch"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/docs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/example/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/packages/cli/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/cli",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "cli tool for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "bin": {
14 | "rne": "./dist/index.js"
15 | },
16 | "scripts": {
17 | "prepack": "yarn cleanup && yarn build",
18 | "cleanup": "rimraf ./dist",
19 | "typecheck": "tsc --noEmit",
20 | "build": "node build/index.js",
21 | "start": "tsc && node ./dist/index.js"
22 | },
23 | "directories": {
24 | "lib": "lib",
25 | "test": "__tests__"
26 | },
27 | "files": [
28 | "lib",
29 | "dist",
30 | "scripts"
31 | ],
32 | "publishConfig": {
33 | "access": "public"
34 | },
35 | "repository": {
36 | "type": "git",
37 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
38 | },
39 | "bugs": {
40 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
41 | },
42 | "dependencies": {
43 | "@react-native-esbuild/config": "workspace:*",
44 | "@react-native-esbuild/core": "workspace:*",
45 | "@react-native-esbuild/dev-server": "workspace:*",
46 | "@react-native-esbuild/plugins": "workspace:*",
47 | "@react-native-esbuild/utils": "workspace:*",
48 | "yargs": "^17.7.2",
49 | "zod": "^3.22.2"
50 | },
51 | "devDependencies": {
52 | "@types/yargs": "^17.0.24",
53 | "esbuild": "^0.25.5",
54 | "typescript": "^5.8.3"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/transformer/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/transformer",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "transformer for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild",
9 | "sucrase",
10 | "babel"
11 | ],
12 | "author": "leegeunhyeok ",
13 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
14 | "license": "MIT",
15 | "main": "dist/index.js",
16 | "types": "dist/lib/index.d.ts",
17 | "scripts": {
18 | "prepack": "yarn cleanup && yarn build",
19 | "cleanup": "rimraf ./dist",
20 | "typecheck": "tsc --noEmit",
21 | "build": "node build/index.js && tsc"
22 | },
23 | "directories": {
24 | "lib": "lib",
25 | "test": "__tests__"
26 | },
27 | "files": [
28 | "lib"
29 | ],
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
36 | },
37 | "bugs": {
38 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
39 | },
40 | "devDependencies": {
41 | "@types/md5": "^2.3.4",
42 | "esbuild": "^0.25.5",
43 | "typescript": "^5.8.3"
44 | },
45 | "dependencies": {
46 | "@babel/core": "^7.25.2",
47 | "@react-native-esbuild/config": "workspace:*",
48 | "@swc/core": "^1.11.29",
49 | "@swc/helpers": "^0.5.17",
50 | "hermes-parser": "^0.28.1",
51 | "md5": "^2.3.0",
52 | "sucrase": "^3.35.0",
53 | "swc-plugin-coverage-instrument": "^0.0.27",
54 | "swc_mut_cjs_exports": "^10.7.0"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/packages/jest/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/jest",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "react-native preset for jest powered by @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild",
9 | "jest"
10 | ],
11 | "author": "leegeunhyeok ",
12 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
13 | "license": "MIT",
14 | "main": "dist/index.js",
15 | "types": "dist/lib/index.d.ts",
16 | "scripts": {
17 | "prepack": "yarn cleanup && yarn build",
18 | "cleanup": "rimraf ./dist",
19 | "typecheck": "tsc --noEmit",
20 | "build": "node build/index.js && tsc"
21 | },
22 | "directories": {
23 | "lib": "lib",
24 | "test": "__tests__"
25 | },
26 | "files": [
27 | "lib",
28 | "dist",
29 | "jest-preset.js"
30 | ],
31 | "publishConfig": {
32 | "access": "public"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
37 | },
38 | "bugs": {
39 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
40 | },
41 | "peerDependencies": {
42 | "react-native": "*"
43 | },
44 | "dependencies": {
45 | "@jest/create-cache-key-function": "^29.7.0",
46 | "@react-native-esbuild/core": "workspace:*",
47 | "@react-native-esbuild/internal": "workspace:*",
48 | "@react-native-esbuild/transformer": "workspace:*",
49 | "md5": "^2.3.0"
50 | },
51 | "devDependencies": {
52 | "@jest/transform": "^29.7.0",
53 | "@types/md5": "^2.3.2",
54 | "esbuild": "^0.25.5",
55 | "typescript": "^5.8.3"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/events/index.ts:
--------------------------------------------------------------------------------
1 | import EventEmitter from 'node:events';
2 | import type {
3 | BundlerAdditionalData,
4 | BuildStatus,
5 | ReportableEvent,
6 | } from '../../types';
7 |
8 | export class BundlerEventEmitter extends EventEmitter {
9 | declare addListener: (
10 | type: EventType,
11 | listener: BundlerEventListener,
12 | ) => this;
13 | declare removeEventListener: (
14 | type: EventType,
15 | listener: BundlerEventListener,
16 | ) => this;
17 | declare on: (
18 | type: EventType,
19 | listener: BundlerEventListener,
20 | ) => this;
21 | declare off: (
22 | type: EventType,
23 | listener: BundlerEventListener,
24 | ) => this;
25 | declare emit: (
26 | type: EventType,
27 | payload: BundlerEventPayload[EventType],
28 | ) => boolean;
29 | }
30 |
31 | export type BundlerEventType =
32 | | 'build-start'
33 | | 'build-end'
34 | | 'build-status-change'
35 | | 'report';
36 |
37 | export type BundlerEventListener = (
38 | payload: BundlerEventPayload[EventType],
39 | ) => void;
40 |
41 | export interface BundlerEventPayload {
42 | 'build-start': { id: number; additionalData?: BundlerAdditionalData };
43 | 'build-end': {
44 | id: number;
45 | revisionId: string;
46 | additionalData?: BundlerAdditionalData;
47 | };
48 | 'build-status-change': BuildStatus & {
49 | id: number;
50 | additionalData?: BundlerAdditionalData;
51 | };
52 | report: ReportableEvent;
53 | }
54 |
--------------------------------------------------------------------------------
/packages/cli/lib/schema/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { SUPPORT_PLATFORMS } from '@react-native-esbuild/config';
3 | import { z } from 'zod';
4 |
5 | const resolvePath = (filepath: string): string =>
6 | path.resolve(process.cwd(), filepath);
7 |
8 | export const baseArgvSchema = z.object({
9 | verbose: z.boolean().optional(),
10 | config: z.string().optional(),
11 | 'reset-cache': z.boolean().optional(),
12 | });
13 |
14 | export const startArgvSchema = z.object({
15 | host: z.string().optional(),
16 | port: z.number().optional(),
17 | 'entry-file': z.string().transform(resolvePath).optional(),
18 | });
19 |
20 | export const serveArgvSchema = z.object({
21 | host: z.string().optional(),
22 | port: z.number().optional(),
23 | dev: z.boolean().optional(),
24 | minify: z.boolean().optional(),
25 | template: z.string().transform(resolvePath).optional(),
26 | 'entry-file': z.string().transform(resolvePath).optional(),
27 | });
28 |
29 | export const bundleArgvSchema = z.object({
30 | /**
31 | * Type infer issue with using `map()`.
32 | *
33 | * ```ts
34 | * // not work
35 | * z.union(SUPPORT_PLATFORMS.map(z.literal)),
36 | * ```
37 | */
38 | platform: z
39 | .union([
40 | z.literal(SUPPORT_PLATFORMS[0]),
41 | z.literal(SUPPORT_PLATFORMS[1]),
42 | z.literal(SUPPORT_PLATFORMS[2]),
43 | ])
44 | .optional(),
45 | dev: z.boolean().optional(),
46 | minify: z.boolean().optional(),
47 | metafile: z.boolean().optional(),
48 | 'entry-file': z.string().transform(resolvePath).optional(),
49 | 'sourcemap-output': z.string().transform(resolvePath).optional(),
50 | 'bundle-output': z.string().transform(resolvePath).optional(),
51 | 'assets-dest': z.string().optional(),
52 | });
53 |
--------------------------------------------------------------------------------
/packages/cli/lib/commands/start.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable quotes -- Allow quote in template literal */
2 | import path from 'node:path';
3 | import { ReactNativeAppServer } from '@react-native-esbuild/dev-server';
4 | import { DEFAULT_ENTRY_POINT } from '@react-native-esbuild/config';
5 | import { enableInteractiveMode, printDebugOptions } from '../helpers';
6 | import { startArgvSchema } from '../schema';
7 | import { presets } from '../presets';
8 | import { logger } from '../shared';
9 | import type { Command } from '../types';
10 |
11 | /**
12 | * Start dev server for native.
13 | */
14 | export const start: Command = async (argv) => {
15 | const startArgv = startArgvSchema.parse(argv);
16 | const entry = path.resolve(startArgv['entry-file'] ?? DEFAULT_ENTRY_POINT);
17 | const serveOptions = {
18 | host: startArgv.host,
19 | port: startArgv.port,
20 | };
21 | logger.debug('start options');
22 | printDebugOptions({ entry, ...serveOptions });
23 |
24 | const server = await new ReactNativeAppServer(serveOptions).initialize(
25 | (bundler) => {
26 | presets.native.forEach(bundler.addPlugin.bind(bundler));
27 | },
28 | );
29 |
30 | await server.listen(() => {
31 | if (
32 | enableInteractiveMode((keyName) => {
33 | switch (keyName) {
34 | case 'r':
35 | logger.info(`sending 'reload' command...`);
36 | server.broadcastCommand('reload');
37 | break;
38 |
39 | case 'd':
40 | logger.info(`sending 'devMenu' command...`);
41 | server.broadcastCommand('devMenu');
42 | break;
43 | }
44 | })
45 | ) {
46 | logger.info(`> press 'r' to reload`);
47 | logger.info(`> press 'd' to open developer menu`);
48 | logger.nl();
49 | }
50 | });
51 | };
52 |
--------------------------------------------------------------------------------
/packages/plugins/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/plugins",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "plugins for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "main": "dist/index.js",
14 | "types": "dist/lib/index.d.ts",
15 | "scripts": {
16 | "prepack": "yarn cleanup && yarn build",
17 | "cleanup": "rimraf ./dist",
18 | "typecheck": "tsc --noEmit",
19 | "build": "node build/index.js && tsc"
20 | },
21 | "directories": {
22 | "lib": "lib",
23 | "test": "__tests__"
24 | },
25 | "files": [
26 | "lib",
27 | "dist"
28 | ],
29 | "publishConfig": {
30 | "access": "public"
31 | },
32 | "repository": {
33 | "type": "git",
34 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
35 | },
36 | "bugs": {
37 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
38 | },
39 | "devDependencies": {
40 | "@faker-js/faker": "^8.1.0",
41 | "@types/invariant": "^2.2.36",
42 | "@types/md5": "^2.3.2",
43 | "deepmerge": "^4.3.1",
44 | "esbuild": "^0.25.5",
45 | "typescript": "^5.8.3"
46 | },
47 | "dependencies": {
48 | "@react-native-esbuild/config": "workspace:*",
49 | "@react-native-esbuild/core": "workspace:*",
50 | "@react-native-esbuild/internal": "workspace:*",
51 | "@react-native-esbuild/transformer": "workspace:*",
52 | "@react-native-esbuild/utils": "workspace:*",
53 | "@svgr/core": "^8.1.0",
54 | "@svgr/plugin-jsx": "^8.1.0",
55 | "image-size": "^1.0.2",
56 | "invariant": "^2.2.4",
57 | "md5": "^2.3.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/example/ios/example/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | example
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | $(CURRENT_PROJECT_VERSION)
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 |
30 | NSAllowsArbitraryLoads
31 |
32 | NSAllowsLocalNetworking
33 |
34 |
35 | NSLocationWhenInUseUsageDescription
36 |
37 | UILaunchStoryboardName
38 | LaunchScreen
39 | UIRequiredDeviceCapabilities
40 |
41 | arm64
42 |
43 | UISupportedInterfaceOrientations
44 |
45 | UIInterfaceOrientationPortrait
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 | UIViewControllerBasedStatusBarAppearance
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/packages/cli/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
2 | import { LogLevel } from '@react-native-esbuild/utils';
3 | import { cli } from './cli';
4 | import * as Commands from './commands';
5 | import { getCommand, handleUncaughtException } from './helpers';
6 | import { baseArgvSchema } from './schema';
7 | import { logger } from './shared';
8 |
9 | (async () => {
10 | const argv = await cli();
11 | const options = baseArgvSchema.parse(argv);
12 | ReactNativeEsbuildBundler.bootstrap(options.config);
13 | ReactNativeEsbuildBundler.setGlobalLogLevel(
14 | options.verbose ? LogLevel.Trace : LogLevel.Info,
15 | );
16 | self.shouldResetCache = options['reset-cache'];
17 |
18 | switch (getCommand(argv)) {
19 | case 'start':
20 | await Commands.start(argv);
21 | break;
22 |
23 | case 'serve':
24 | await Commands.serve(argv);
25 | break;
26 |
27 | case 'bundle':
28 | await Commands.bundle(argv);
29 | break;
30 |
31 | case 'cache':
32 | await Commands.cache(argv, getCommand(argv, 1));
33 | break;
34 |
35 | case 'ram-bundle':
36 | // eslint-disable-next-line quotes -- Log message.
37 | logger.warn(`'ram-bundle' command is not supported`);
38 | process.exit(1);
39 | }
40 | })().catch((error) => {
41 | logger.error('cannot execute command', error as Error);
42 | process.exit(1);
43 | });
44 |
45 | process
46 | .on('unhandledRejection', (reason, promise) => {
47 | logger.error('unhandled rejection at promise', undefined, {
48 | reason,
49 | promise,
50 | });
51 | process.exit(1);
52 | })
53 | .on('uncaughtException', (error) => {
54 | if (!handleUncaughtException(error)) {
55 | logger.error('uncaught exception thrown', error);
56 | }
57 | process.exit(1);
58 | });
59 |
--------------------------------------------------------------------------------
/example/src/screens/MainScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
4 | import { Container, View, H1, P } from 'dripsy';
5 | import { Fade, Button } from '../components';
6 | import type { RootStackProps } from '../navigators/types';
7 | import LogoSvg from '../assets/logo.svg';
8 |
9 | export function MainScreen({
10 | navigation,
11 | }: RootStackProps<'Main'>): React.ReactElement {
12 | const { top, bottom } = useSafeAreaInsets();
13 |
14 | const handlePressStartButton = (): void => {
15 | navigation.navigate('Intro');
16 | };
17 |
18 | return (
19 |
20 |
21 |
22 | React Native Esbuild
23 |
24 | ⚡️ An extremely fast bundler{'\n'}+{'\n'}React Native
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | container: {
37 | flex: 1,
38 | backgroundColor: 'white',
39 | padding: 16,
40 | justifyContent: 'space-between',
41 | },
42 | contentArea: {
43 | flex: 1,
44 | alignItems: 'center',
45 | justifyContent: 'center',
46 | paddingVertical: 24,
47 | gap: 8,
48 | },
49 | title: {
50 | fontSize: 30,
51 | fontWeight: '600',
52 | textAlign: 'center',
53 | color: '#222',
54 | },
55 | subTitle: {
56 | fontSize: 20,
57 | fontWeight: '600',
58 | textAlign: 'center',
59 | color: '#555',
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/core",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "core of @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "module": "lib/index.ts",
14 | "main": "dist/index.js",
15 | "types": "dist/lib/index.d.ts",
16 | "scripts": {
17 | "prepack": "yarn cleanup && yarn build",
18 | "cleanup": "rimraf ./dist",
19 | "typecheck": "tsc --noEmit",
20 | "build": "node build/index.js && tsc"
21 | },
22 | "directories": {
23 | "lib": "lib",
24 | "test": "__tests__"
25 | },
26 | "files": [
27 | "lib",
28 | "dist"
29 | ],
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
36 | },
37 | "bugs": {
38 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
39 | },
40 | "engines": {
41 | "node": ">=16"
42 | },
43 | "dependencies": {
44 | "@react-native-esbuild/config": "workspace:*",
45 | "@react-native-esbuild/internal": "workspace:*",
46 | "@react-native-esbuild/transformer": "workspace:*",
47 | "@react-native-esbuild/utils": "workspace:*",
48 | "chokidar": "^3.5.3",
49 | "deepmerge": "^4.3.1",
50 | "esbuild": "^0.25.5",
51 | "invariant": "^2.2.4",
52 | "ora": "^5.4.1"
53 | },
54 | "peerDependencies": {
55 | "react-native": "*"
56 | },
57 | "devDependencies": {
58 | "@babel/core": "^7.23.2",
59 | "@faker-js/faker": "^8.1.0",
60 | "@swc/core": "^1.11.29",
61 | "typescript": "^5.8.3"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/plugins/statusPlugin/statusPlugin.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import type { BuildResult } from 'esbuild';
3 | import type {
4 | ReactNativeEsbuildPluginCreator,
5 | PluginContext,
6 | BuildStatus,
7 | } from '../../../types';
8 | import { StatusLogger } from './StatusLogger';
9 |
10 | const NAME = 'build-status-plugin';
11 |
12 | export const createBuildStatusPlugin: ReactNativeEsbuildPluginCreator<{
13 | onStart: (context: PluginContext) => void;
14 | onUpdate: (buildState: BuildStatus, context: PluginContext) => void;
15 | onEnd: (
16 | data: {
17 | result: BuildResult;
18 | success: boolean;
19 | },
20 | context: PluginContext,
21 | ) => void;
22 | }> = (context, config) => ({
23 | name: NAME,
24 | setup: (build): void => {
25 | const statusLogger = new StatusLogger(context);
26 | const filter = /.*/;
27 | let statusLoaded = false;
28 |
29 | build.onStart(async () => {
30 | if (!statusLoaded) {
31 | await statusLogger.loadStatus();
32 | statusLoaded = true;
33 | }
34 | statusLogger.setup();
35 | config?.onStart(context);
36 | });
37 |
38 | build.onResolve({ filter }, (args) => {
39 | const isRelative = args.path.startsWith('.');
40 | statusLogger.onResolve(
41 | isRelative ? path.resolve(args.resolveDir, args.path) : args.path,
42 | );
43 | config?.onUpdate(statusLogger.getStatus(), context);
44 | return null;
45 | });
46 |
47 | build.onLoad({ filter }, () => {
48 | statusLogger.onLoad();
49 | return null;
50 | });
51 |
52 | build.onEnd(async (result: BuildResult) => {
53 | const success = await statusLogger.summary(result);
54 | await statusLogger.persistStatus();
55 | config?.onEnd({ result, success }, context);
56 | });
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/com/example/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example;
2 | import android.app.Application
3 | import com.facebook.react.PackageList
4 | import com.facebook.react.ReactApplication
5 | import com.facebook.react.ReactHost
6 | import com.facebook.react.ReactNativeHost
7 | import com.facebook.react.ReactPackage
8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
9 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
10 | import com.facebook.react.defaults.DefaultReactNativeHost
11 | import com.facebook.react.soloader.OpenSourceMergedSoMapping
12 | import com.facebook.soloader.SoLoader
13 |
14 | class MainApplication : Application(), ReactApplication {
15 |
16 | override val reactNativeHost: ReactNativeHost =
17 | object : DefaultReactNativeHost(this) {
18 | override fun getPackages(): List =
19 | PackageList(this).packages.apply {
20 | // Packages that cannot be autolinked yet can be added manually here, for example:
21 | // add(MyReactNativePackage())
22 | }
23 |
24 | override fun getJSMainModuleName(): String = "index"
25 |
26 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
27 |
28 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
29 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
30 | }
31 |
32 | override val reactHost: ReactHost
33 | get() = getDefaultReactHost(applicationContext, reactNativeHost)
34 |
35 | override fun onCreate() {
36 | super.onCreate()
37 | SoLoader.init(this, OpenSourceMergedSoMapping)
38 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
39 | // If you opted-in for the New Architecture, we load the native entry point for this app.
40 | load()
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/pages/troubleshooting/new-architecture.mdx:
--------------------------------------------------------------------------------
1 | import { Callout } from 'nextra/components'
2 |
3 | # New Architecture
4 |
5 |
6 | [New Architecture](https://reactnative.dev/docs/the-new-architecture/landing-page) is experimental.
7 |
8 |
9 | ## react-native-reanimated
10 |
11 | > ReferenceError: Property 'nativeFabricUIManager' doesn't exist
12 |
13 | - Some [issue](https://github.com/leegeunhyeok/react-native-esbuild/issues/16#issuecomment-1730042378) on `react-native-reanimated` + `Fabric`
14 | - To resolve(temporary solution), open `node_modules/react-native-reanimated/src/reanimated2/fabricUtils.ts` and change `react-native/Libraries/Renderer/shims/ReactFabric` to inline lazy require.
15 |
16 | ```diff
17 | let findHostInstance_DEPRECATED: (ref: React.Component) => HostInstance;
18 | - if (global._IS_FABRIC) {
19 | - try {
20 | - findHostInstance_DEPRECATED =
21 | - // eslint-disable-next-line @typescript-eslint/no-var-requires
22 | - require('react-native/Libraries/Renderer/shims/ReactFabric').findHostInstance_DEPRECATED;
23 | - } catch (e) {
24 | - throw new Error(
25 | - '[Reanimated] Cannot import `findHostInstance_DEPRECATED`.'
26 | - );
27 | - }
28 | - }
29 |
30 | export function getShadowNodeWrapperFromRef(
31 | ref: React.Component
32 | ): ShadowNodeWrapper {
33 | + if (global._IS_FABRIC && !findHostInstance_DEPRECATED) {
34 | + try {
35 | + findHostInstance_DEPRECATED =
36 | + // eslint-disable-next-line @typescript-eslint/no-var-requires
37 | + require('react-native/Libraries/Renderer/shims/ReactFabric').findHostInstance_DEPRECATED;
38 | + } catch (e) {
39 | + throw new Error(
40 | + '[Reanimated] Cannot import `findHostInstance_DEPRECATED`.'
41 | + );
42 | + }
43 | + }
44 | return findHostInstance_DEPRECATED(ref)._internalInstanceHandle.stateNode
45 | .node;
46 | }
47 | ```
48 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 |
25 | # Use this property to specify which architecture you want to build.
26 | # You can also override it from the CLI using
27 | # ./gradlew -PreactNativeArchitectures=x86_64
28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
29 |
30 | # Use this property to enable support to the new architecture.
31 | # This will allow you to use TurboModules and the Fabric render in
32 | # your application. You should enable this flag either if you want
33 | # to write custom TurboModules/Fabric components OR use libraries that
34 | # are providing them.
35 | newArchEnabled=true
36 |
37 | # Use this property to enable or disable the Hermes JS engine.
38 | # If set to false, you will be using JSC instead.
39 | hermesEnabled=true
40 |
--------------------------------------------------------------------------------
/packages/dev-server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@react-native-esbuild/dev-server",
3 | "version": "0.1.0-beta.12",
4 | "type": "module",
5 | "description": "development server for @react-native-esbuild",
6 | "keywords": [
7 | "react-native",
8 | "esbuild"
9 | ],
10 | "author": "leegeunhyeok ",
11 | "homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
12 | "license": "MIT",
13 | "main": "dist/index.js",
14 | "types": "dist/lib/index.d.ts",
15 | "scripts": {
16 | "prepack": "yarn cleanup && yarn build",
17 | "cleanup": "rimraf ./dist",
18 | "typecheck": "tsc --noEmit",
19 | "build": "node build/index.js && tsc"
20 | },
21 | "directories": {
22 | "lib": "lib",
23 | "test": "__tests__"
24 | },
25 | "files": [
26 | "lib",
27 | "dist",
28 | "static"
29 | ],
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
36 | },
37 | "bugs": {
38 | "url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
39 | },
40 | "devDependencies": {
41 | "@faker-js/faker": "^8.1.0",
42 | "@types/connect": "^3.4.35",
43 | "@types/invariant": "^2.2.36",
44 | "@types/mime": "^3.0.1",
45 | "@types/ws": "^8.18.1",
46 | "esbuild": "^0.25.5",
47 | "typescript": "^5.8.3"
48 | },
49 | "dependencies": {
50 | "@react-native-community/cli-server-api": "^11.3.6",
51 | "@react-native-esbuild/config": "workspace:*",
52 | "@react-native-esbuild/core": "workspace:*",
53 | "@react-native-esbuild/internal": "workspace:*",
54 | "@react-native-esbuild/symbolicate": "workspace:*",
55 | "@react-native-esbuild/utils": "workspace:*",
56 | "invariant": "^2.2.4",
57 | "metro-inspector-proxy": "^0.78.0",
58 | "mime": "^3.0.0",
59 | "ws": "^8.18.2",
60 | "zod": "^3.22.2"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/example/src/screens/IntroScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Linking } from 'react-native';
3 | import { styled, Container, ScrollView, View, Image, Text, P } from 'dripsy';
4 | import { Section, Button } from '../components';
5 | import Logo from '../assets/logo.png';
6 |
7 | const Description = styled(P, {
8 | defaultVariant: 'text.secondary',
9 | })();
10 |
11 | const LogoImage = styled(Image)({
12 | width: 64,
13 | height: 64,
14 | });
15 |
16 | const FEATURES = [
17 | '⚡️ Blazing Fast Build',
18 | '🌳 Supports Tree Shaking',
19 | '💾 In-memory & Local File System Caching',
20 | '🎨 Flexible & Extensible',
21 | '🔥 Supports JSC & Hermes Runtime',
22 | '🔄 Supports Live Reload',
23 | '🐛 Supports Debugging(Flipper, Chrome Debugger)',
24 | '🌍 Supports All Platforms(Android, iOS, Web)',
25 | '✨ New Architecture Ready',
26 | ];
27 |
28 | export function IntroScreen(): React.ReactElement {
29 | const handlePressGitHub = (): void => {
30 | Linking.openURL(
31 | 'https://github.com/leegeunhyeok/react-native-esbuild',
32 | ).catch(() => null);
33 | };
34 |
35 | return (
36 |
37 |
38 |
41 |
42 | {FEATURES.map((content, index) => (
43 | {content}
44 | ))}
45 |
46 |
47 | Report bugs, request features, or anything
48 |
49 |
50 |
51 | This project is under development.
52 | CHECK & TEST BEFORE USING IN PRODUCTION
53 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import type { OnResolveArgs, ResolveResult } from 'esbuild';
3 | import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
4 | import { getDevServerPublicPath } from '@react-native-esbuild/config';
5 | import { logger } from '../shared';
6 | import { generateIndexPage } from './helpers';
7 |
8 | const NAME = 'react-native-web-plugin';
9 | const RESOLVE_PATTERNS = [
10 | // For relative path import of initializeCore.
11 | /node_modules\/react-native\//,
12 | // For import via package name.
13 | /^react-native$/,
14 | ];
15 |
16 | export const createReactNativeWebPlugin: ReactNativeEsbuildPluginCreator = (
17 | context,
18 | ) => ({
19 | name: NAME,
20 | setup: (build): void => {
21 | const { root, platform, outfile, mode } = context;
22 | const { template, placeholders } = context.config.web ?? {};
23 | const destination =
24 | mode === 'watch' ? getDevServerPublicPath(root) : path.dirname(outfile);
25 | const bundleFilename = path.basename(outfile);
26 |
27 | if (platform !== 'web') return;
28 |
29 | const resolveReactNativeWeb = (
30 | args: OnResolveArgs,
31 | ): Promise => {
32 | return build.resolve('react-native-web', {
33 | kind: args.kind,
34 | resolveDir: args.resolveDir,
35 | importer: args.importer,
36 | });
37 | };
38 |
39 | RESOLVE_PATTERNS.forEach((pattern) => {
40 | build.onResolve({ filter: pattern }, resolveReactNativeWeb);
41 | });
42 |
43 | build.onEnd(async (result) => {
44 | if (result.errors.length) return;
45 | if (!template) {
46 | logger.warn('template is not specified');
47 | return;
48 | }
49 |
50 | await generateIndexPage(template, destination, {
51 | ...placeholders,
52 | _bundle: bundleFilename.endsWith('.js')
53 | ? bundleFilename
54 | : `${bundleFilename}.js`,
55 | });
56 | });
57 | },
58 | });
59 |
--------------------------------------------------------------------------------
/packages/transformer/lib/helpers/transformer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | transformWithBabel,
3 | transformSyncWithBabel,
4 | transformWithSwc,
5 | transformSyncWithSwc,
6 | } from '../transformer';
7 | import type {
8 | TransformRuleBase,
9 | TransformerContext,
10 | SwcTransformRule,
11 | BabelTransformRule,
12 | TransformerOptionsPreset,
13 | } from '../types';
14 |
15 | const ruleOptionsToPreset = (
16 | options: TransformRuleBase['options'],
17 | code: string,
18 | ): TransformerOptionsPreset => {
19 | return options instanceof Function
20 | ? (context) => options(context.path, code)
21 | : () => options;
22 | };
23 |
24 | export const transformByBabelRule = (
25 | rule: BabelTransformRule,
26 | code: string,
27 | context: TransformerContext,
28 | ): Promise => {
29 | return rule.test(context.path, code)
30 | ? transformWithBabel(code, context, ruleOptionsToPreset(rule.options, code))
31 | : Promise.resolve(null);
32 | };
33 |
34 | export const transformSyncByBabelRule = (
35 | rule: BabelTransformRule,
36 | code: string,
37 | context: TransformerContext,
38 | ): string | null => {
39 | return rule.test(context.path, code)
40 | ? transformSyncWithBabel(
41 | code,
42 | context,
43 | ruleOptionsToPreset(rule.options, code),
44 | )
45 | : null;
46 | };
47 |
48 | export const transformBySwcRule = (
49 | rule: SwcTransformRule,
50 | code: string,
51 | context: TransformerContext,
52 | ): Promise => {
53 | return rule.test(context.path, code)
54 | ? transformWithSwc(code, context, ruleOptionsToPreset(rule.options, code))
55 | : Promise.resolve(null);
56 | };
57 |
58 | export const transformSyncBySwcRule = (
59 | rule: SwcTransformRule,
60 | code: string,
61 | context: TransformerContext,
62 | ): string | null => {
63 | return rule.test(context.path, code)
64 | ? transformSyncWithSwc(
65 | code,
66 | context,
67 | ruleOptionsToPreset(rule.options, code),
68 | )
69 | : null;
70 | };
71 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/packages/dev-server/lib/middlewares/__tests__/mocks.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | IncomingMessage,
3 | IncomingHttpHeaders,
4 | ServerResponse,
5 | } from 'node:http';
6 | import type { FileHandle } from 'node:fs/promises';
7 | import { faker } from '@faker-js/faker';
8 | import type { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
9 |
10 | interface MockedRequestParams {
11 | url: string | undefined;
12 | headers?: IncomingHttpHeaders;
13 | }
14 |
15 | interface MockedFileHandlerParams {
16 | data: string;
17 | size: number;
18 | isDirectory: boolean;
19 | }
20 |
21 | interface MockedBundlerParams {
22 | bundle: string;
23 | hasError: boolean;
24 | }
25 |
26 | export const getMockedRequest = ({
27 | url,
28 | headers = { accept: '' },
29 | }: MockedRequestParams): IncomingMessage => {
30 | return {
31 | url,
32 | headers,
33 | } as IncomingMessage;
34 | };
35 |
36 | export const getMockedResponse = (): ServerResponse => {
37 | const response = {
38 | setHeader: jest.fn().mockImplementation(() => response),
39 | writeHead: jest.fn().mockImplementation(() => response),
40 | end: jest.fn().mockImplementation(() => response),
41 | } as unknown as ServerResponse;
42 |
43 | return response;
44 | };
45 |
46 | export const getMockedFileHandler = ({
47 | data,
48 | size,
49 | isDirectory,
50 | }: MockedFileHandlerParams): FileHandle => {
51 | return {
52 | stat: jest.fn().mockReturnValue({
53 | size,
54 | isDirectory: jest.fn().mockReturnValue(isDirectory),
55 | }),
56 | readFile: jest.fn().mockReturnValue(Promise.resolve(data)),
57 | close: jest.fn().mockReturnValue(Promise.resolve()),
58 | } as unknown as FileHandle;
59 | };
60 |
61 | export const getMockedBundler = ({
62 | bundle,
63 | hasError,
64 | }: MockedBundlerParams): ReactNativeEsbuildBundler => {
65 | return {
66 | getBundleResult: jest.fn().mockReturnValue(
67 | Promise.resolve({
68 | result: hasError
69 | ? null
70 | : { source: bundle, bundledAt: faker.date.past() },
71 | error: hasError ? new Error() : null,
72 | }),
73 | ),
74 | on: jest.fn(),
75 | off: jest.fn(),
76 | } as unknown as ReactNativeEsbuildBundler;
77 | };
78 |
--------------------------------------------------------------------------------
/packages/plugins/lib/assetRegisterPlugin/helpers/fs.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs/promises';
2 | import path from 'node:path';
3 | import type { PluginContext } from '@react-native-esbuild/core';
4 | import type { Asset } from '@react-native-esbuild/internal';
5 | import { logger } from '../../shared';
6 | import {
7 | getAndroidAssetDestinationPath,
8 | getAssetDestinationPath,
9 | resolveAssetPath,
10 | } from './path';
11 |
12 | /**
13 | * Copy assets to platform specified destination.
14 | *
15 | * @see {@link https://github.com/react-native-community/cli/blob/v11.3.6/packages/cli-plugin-metro/src/commands/bundle/assetPathUtils.ts}
16 | */
17 | export const copyAssetsToDestination = async (
18 | context: PluginContext,
19 | assets: Asset[],
20 | ): Promise => {
21 | const { assetsDir, mode } = context;
22 | if (mode === 'watch') return;
23 |
24 | if (!assetsDir) {
25 | logger.warn('asset destination is not set');
26 | return;
27 | }
28 |
29 | logger.debug('copy assets to target destination');
30 |
31 | const mkdirWithAssertPath = (targetPath: string): Promise => {
32 | const dirname = path.dirname(targetPath);
33 | return fs
34 | .access(dirname)
35 | .catch(() => fs.mkdir(dirname, { recursive: true }))
36 | .then(() => void 0);
37 | };
38 |
39 | return Promise.all(
40 | assets.map((asset): Promise => {
41 | return Promise.all(
42 | asset.scales.map(async (scale): Promise => {
43 | if (context.platform !== 'android') {
44 | const from = await resolveAssetPath(asset, scale);
45 | const to = path.join(
46 | assetsDir,
47 | getAssetDestinationPath(asset, scale),
48 | );
49 | await mkdirWithAssertPath(to);
50 | return fs.copyFile(from, to);
51 | }
52 |
53 | const from = await resolveAssetPath(asset, scale);
54 | const to = path.join(
55 | assetsDir,
56 | getAndroidAssetDestinationPath(asset, scale),
57 | );
58 | await mkdirWithAssertPath(to);
59 | await fs.copyFile(from, to);
60 | }),
61 | ).then(() => void 0);
62 | }),
63 | ).then(() => void 0);
64 | };
65 |
--------------------------------------------------------------------------------
/example/ios/exampleTests/exampleTests.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | #import
5 | #import
6 |
7 | #define TIMEOUT_SECONDS 600
8 | #define TEXT_TO_LOOK_FOR @"Welcome to React"
9 |
10 | @interface exampleTests : XCTestCase
11 |
12 | @end
13 |
14 | @implementation exampleTests
15 |
16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test
17 | {
18 | if (test(view)) {
19 | return YES;
20 | }
21 | for (UIView *subview in [view subviews]) {
22 | if ([self findSubviewInView:subview matching:test]) {
23 | return YES;
24 | }
25 | }
26 | return NO;
27 | }
28 |
29 | - (void)testRendersWelcomeScreen
30 | {
31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController];
32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
33 | BOOL foundElement = NO;
34 |
35 | __block NSString *redboxError = nil;
36 | #ifdef DEBUG
37 | RCTSetLogFunction(
38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) {
39 | if (level >= RCTLogLevelError) {
40 | redboxError = message;
41 | }
42 | });
43 | #endif
44 |
45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
48 |
49 | foundElement = [self findSubviewInView:vc.view
50 | matching:^BOOL(UIView *view) {
51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) {
52 | return YES;
53 | }
54 | return NO;
55 | }];
56 | }
57 |
58 | #ifdef DEBUG
59 | RCTSetLogFunction(RCTDefaultLogFunction);
60 | #endif
61 |
62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
64 | }
65 |
66 | @end
67 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('node:path');
2 |
3 | const project = resolve(__dirname, 'tsconfig.json');
4 |
5 | /** @type { import('eslint').ESLint.ConfigData } */
6 | module.exports = {
7 | root: true,
8 | env: {
9 | node: true,
10 | },
11 | plugins: ['prettier'],
12 | extends: [
13 | require.resolve('@vercel/style-guide/eslint/node'),
14 | require.resolve('@vercel/style-guide/eslint/typescript'),
15 | ],
16 | parserOptions: {
17 | project,
18 | },
19 | settings: {
20 | 'import/resolver': {
21 | typescript: {
22 | project,
23 | },
24 | },
25 | },
26 | overrides: [
27 | {
28 | files: ['*.ts?(x)', '*.js?(x)'],
29 | rules: {
30 | semi: ['error', 'always'],
31 | quotes: ['error', 'single'],
32 | 'new-cap': 'off',
33 | 'object-curly-spacing': ['error', 'always'],
34 | 'array-bracket-spacing': 'off',
35 | 'unicorn/filename-case': 'off',
36 | 'import/no-named-as-default-member': 'off',
37 | 'no-bitwise': 'off',
38 | 'no-param-reassign': 'off',
39 | 'eqeqeq': 'off',
40 | 'eslint-comments/disable-enable-pair': 'off',
41 | '@typescript-eslint/prefer-reduce-type-parameter': 'off',
42 | '@typescript-eslint/no-shadow': 'off',
43 | '@typescript-eslint/no-floating-promises': 'off',
44 | '@typescript-eslint/no-throw-literal': 'off',
45 | '@typescript-eslint/no-unsafe-enum-comparison': 'off',
46 | },
47 | },
48 | {
49 | files: ['**/build/*.js'],
50 | rules: {
51 | 'no-console': 'off',
52 | },
53 | },
54 | {
55 | files: ['packages/jest/lib/**/*.ts'],
56 | rules: {
57 | 'import/no-default-export': 'off',
58 | 'import/no-named-as-default': 'off',
59 | },
60 | },
61 | {
62 | files: ['example/**/*'],
63 | rules: {
64 | 'no-console': 'off',
65 | 'unicorn/filename-case': 'off',
66 | },
67 | },
68 | {
69 | files: ['**/*.test.ts'],
70 | rules: {
71 | '@typescript-eslint/no-unsafe-assignment': 'off',
72 | '@typescript-eslint/unbound-method': 'off',
73 | },
74 | },
75 | {
76 | files: ['**/*.config.[tj]s'],
77 | rules: {
78 | 'import/no-default-export': 'off',
79 | },
80 | },
81 | ],
82 | };
83 |
--------------------------------------------------------------------------------
/packages/config/lib/common/__tests__/common.test.ts:
--------------------------------------------------------------------------------
1 | import { getIdByOptions, getBuildStatusCachePath } from '../core';
2 | import { OptionFlag } from '../../types';
3 |
4 | const BASE_OPTIONS = {
5 | outfile: '',
6 | entry: '',
7 | metafile: false,
8 | } as const;
9 |
10 | const ROOT_DIR = '/root';
11 |
12 | describe.each([
13 | [
14 | { platform: 'android', dev: false, minify: false },
15 | OptionFlag.PlatformAndroid,
16 | ],
17 | [
18 | { platform: 'android', dev: true, minify: false },
19 | OptionFlag.PlatformAndroid | OptionFlag.Dev,
20 | ],
21 | [
22 | { platform: 'android', dev: false, minify: true },
23 | OptionFlag.PlatformAndroid | OptionFlag.Minify,
24 | ],
25 | [
26 | { platform: 'android', dev: true, minify: true },
27 | OptionFlag.PlatformAndroid | OptionFlag.Dev | OptionFlag.Minify,
28 | ],
29 | [{ platform: 'ios', dev: false, minify: false }, OptionFlag.PlatformIos],
30 | [
31 | { platform: 'ios', dev: true, minify: false },
32 | OptionFlag.PlatformIos | OptionFlag.Dev,
33 | ],
34 | [
35 | { platform: 'ios', dev: false, minify: true },
36 | OptionFlag.PlatformIos | OptionFlag.Minify,
37 | ],
38 | [
39 | { platform: 'ios', dev: true, minify: true },
40 | OptionFlag.PlatformIos | OptionFlag.Dev | OptionFlag.Minify,
41 | ],
42 | [{ platform: 'web', dev: false, minify: false }, OptionFlag.PlatformWeb],
43 | [
44 | { platform: 'web', dev: true, minify: false },
45 | OptionFlag.PlatformWeb | OptionFlag.Dev,
46 | ],
47 | [
48 | { platform: 'web', dev: false, minify: true },
49 | OptionFlag.PlatformWeb | OptionFlag.Minify,
50 | ],
51 | [
52 | { platform: 'web', dev: true, minify: true },
53 | OptionFlag.PlatformWeb | OptionFlag.Dev | OptionFlag.Minify,
54 | ],
55 | ] as const)('getIdByOptions', (options, expected) => {
56 | const dev = options.dev ? 'true' : 'false';
57 | const minify = options.minify ? 'true' : 'false';
58 |
59 | describe(`platform: ${options.platform}, dev: ${dev}, minify: ${minify}`, () => {
60 | it(`should bitwise value is ${expected}`, () => {
61 | expect(getIdByOptions({ ...BASE_OPTIONS, ...options })).toEqual(expected);
62 | });
63 | });
64 | });
65 |
66 | describe('getBuildStatusCachePath', () => {
67 | it('should match snapshot', () => {
68 | expect(getBuildStatusCachePath(ROOT_DIR)).toMatchSnapshot();
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/docs/pages/configuration/custom-transformation.md:
--------------------------------------------------------------------------------
1 | # Custom Transformation
2 |
3 | ## Babel
4 |
5 | Add transform rules to `transformer.additionalTransformRules.babel`.
6 |
7 | ```ts
8 | // react-native-esbuild.config.js
9 | exports.default = {
10 | transformer: {
11 | additionalTransformRules: {
12 | babel: [
13 | {
14 | /**
15 | * @param path file path
16 | * @param code raw source code
17 | * @returns true: transform, false: do not transform
18 | **/
19 | test: (path: string, code: string) => {
20 | return shouldApplyTransformation;
21 | },
22 | /**
23 | * Babel options
24 | **/
25 | options: babelOptions,
26 | // it can be function that return babel options.
27 | options: (path: string, code: string) => babelOptions,
28 | },
29 | ],
30 | },
31 | },
32 | };
33 | ```
34 |
35 | ## Swc
36 |
37 | Add transform rules to `transformer.additionalTransformRules.swc`.
38 |
39 | ```ts
40 | // react-native-esbuild.config.js
41 | exports.default = {
42 | transformer: {
43 | additionalTransformRules: {
44 | swc: [
45 | {
46 | /**
47 | * @param path file path
48 | * @param code raw source code
49 | * @returns true: transform, false: do not transform
50 | **/
51 | test: (path: string, code: string) => {
52 | return shouldApplyTransformation;
53 | },
54 | /**
55 | * Swc options
56 | **/
57 | options: swcOptions,
58 | // it can be function that return swc options.
59 | options: (path: string, code: string) => swcOptions,
60 | },
61 | ],
62 | },
63 | },
64 | };
65 | ```
66 |
67 | ## Usage
68 |
69 | ```js
70 | // react-native-esbuild.config.js
71 | exports.default = {
72 | transformer: {
73 | additionalTransformRules: {
74 | babel: [
75 | {
76 | test: (path, code) => {
77 | return (
78 | /node_modules\/react-native-reanimated\//.test(path) ||
79 | code.includes('react-native-reanimated')
80 | );
81 | },
82 | options: {
83 | plugins: ['react-native-reanimated/plugin'],
84 | babelrc: false,
85 | },
86 | },
87 | ],
88 | },
89 | },
90 | };
91 | ```
92 |
93 |
--------------------------------------------------------------------------------
/packages/dev-server/lib/helpers/request.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'node:url';
2 | import { z } from 'zod';
3 | import type { BundleOptions } from '@react-native-esbuild/config';
4 | import { BundleRequestType } from '../types';
5 |
6 | export type ParsedBundleOptions = z.infer;
7 |
8 | const toBoolean = (val: z.infer): boolean => val === 'true';
9 |
10 | const boolean = z.union([z.literal('true'), z.literal('false')]);
11 | const bundleSearchParamSchema = z
12 | .object({
13 | // Required field.
14 | platform: z.union([
15 | z.literal('android'),
16 | z.literal('ios'),
17 | z.literal('web'),
18 | ]),
19 | // Optional fields.
20 | dev: boolean.default('true').transform(toBoolean),
21 | minify: boolean.default('false').transform(toBoolean),
22 | runModule: boolean.default('false').transform(toBoolean),
23 | })
24 | .required();
25 |
26 | export const parseBundleOptionsFromRequestUrl = (
27 | requestUrl: string | undefined,
28 | ): { type: BundleRequestType; bundleOptions: ParsedBundleOptions | null } => {
29 | if (!requestUrl) {
30 | return { type: BundleRequestType.Unknown, bundleOptions: null };
31 | }
32 |
33 | const { pathname, query } = parse(requestUrl, true);
34 | if (typeof pathname !== 'string') {
35 | return { type: BundleRequestType.Unknown, bundleOptions: null };
36 | }
37 |
38 | // eslint-disable-next-line no-nested-ternary -- Allow nested ternary operator.
39 | const type = pathname.endsWith('.bundle')
40 | ? BundleRequestType.Bundle
41 | : pathname.endsWith('.map')
42 | ? BundleRequestType.Sourcemap
43 | : BundleRequestType.Unknown;
44 |
45 | return {
46 | type,
47 | bundleOptions:
48 | type === BundleRequestType.Unknown
49 | ? null
50 | : bundleSearchParamSchema.parse(query),
51 | };
52 | };
53 |
54 | export const parseBundleOptionsForWeb = (
55 | bundleOptions: Partial,
56 | type: 'bundle' | 'sourcemap',
57 | ): { type: BundleRequestType; bundleOptions: ParsedBundleOptions | null } => {
58 | return {
59 | type:
60 | type === 'bundle'
61 | ? BundleRequestType.Bundle
62 | : BundleRequestType.Sourcemap,
63 | bundleOptions: bundleSearchParamSchema.parse({
64 | ...Object.entries(bundleOptions).reduce((prev, [key, value]) => {
65 | return { ...prev, [key]: value.toString() };
66 | }, {}),
67 | platform: 'web',
68 | }),
69 | };
70 | };
71 |
--------------------------------------------------------------------------------
/packages/core/lib/bundler/helpers/internal.ts:
--------------------------------------------------------------------------------
1 | import type { BuildOptions } from 'esbuild';
2 | import { getPreludeScript } from '@react-native-esbuild/internal';
3 | import type { TransformerContext } from '@react-native-esbuild/transformer';
4 | import {
5 | stripFlowWithSucrase,
6 | minifyWithSwc,
7 | swcPresets,
8 | } from '@react-native-esbuild/transformer';
9 | import type { BundleOptions } from '@react-native-esbuild/config';
10 |
11 | export const getTransformedPreludeScript = async (
12 | bundleOptions: BundleOptions,
13 | root: string,
14 | ): Promise => {
15 | // Dummy context
16 | const context: TransformerContext = {
17 | root,
18 | path: '',
19 | id: 0,
20 | dev: bundleOptions.dev,
21 | entry: bundleOptions.entry,
22 | };
23 | const preludeScript = await getPreludeScript(bundleOptions, root);
24 |
25 | /**
26 | * Remove `"use strict";` added by sucrase.
27 | * @see {@link https://github.com/alangpierce/sucrase/issues/787#issuecomment-1483934492}
28 | */
29 | const strippedScript = stripFlowWithSucrase(preludeScript, context)
30 | .replace(/"use strict";/, '')
31 | .trim();
32 |
33 | return bundleOptions.minify
34 | ? minifyWithSwc(strippedScript, context, swcPresets.getMinifyPreset())
35 | : strippedScript;
36 | };
37 |
38 | export const getResolveExtensionsOption = (
39 | bundleOptions: BundleOptions,
40 | sourceExtensions: string[],
41 | assetExtensions: string[],
42 | ): BuildOptions['resolveExtensions'] => {
43 | const extensions = [...sourceExtensions, ...assetExtensions];
44 | /**
45 | * Platform specified extensions for resolve priority.
46 | *
47 | * (High ~ Low)
48 | * 1. `filename..ext`
49 | * 2. `filename.native.ext` (only platform is `android` or `ios`)
50 | * 3. `filename.react-native.ext` (only platform is `android` or `ios`)
51 | * 4. `filename.ext`
52 | */
53 | return [
54 | bundleOptions.platform,
55 | ...(bundleOptions.platform === 'web' ? [] : ['native', 'react-native']),
56 | ]
57 | .filter(Boolean)
58 | .map((platform) => extensions.map((ext) => `.${platform}${ext}`))
59 | .concat(extensions)
60 | .flat();
61 | };
62 |
63 | export const getLoaderOption = (
64 | assetExtensions: string[],
65 | ): BuildOptions['loader'] => {
66 | /**
67 | * Loader option for file loader interprets the assets as file.
68 | */
69 | return Object.fromEntries(
70 | assetExtensions.map((ext) => [ext, 'file'] as const),
71 | );
72 | };
73 |
--------------------------------------------------------------------------------
/packages/config/lib/common/core.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import {
3 | DEFAULT_ENTRY_POINT,
4 | DEFAULT_OUTFILE,
5 | LOCAL_CACHE_DIR,
6 | } from '../shares';
7 | import { OptionFlag, type BundleOptions } from '../types';
8 |
9 | export const combineWithDefaultBundleOptions = (
10 | options: Partial,
11 | ): BundleOptions => {
12 | if (!options.platform) {
13 | throw new Error('platform is required');
14 | }
15 |
16 | return {
17 | ...options,
18 | platform: options.platform,
19 | minify: options.minify ?? false,
20 | dev: options.dev ?? true,
21 | entry: path.resolve(options.entry ?? DEFAULT_ENTRY_POINT),
22 | outfile: path.resolve(options.outfile ?? DEFAULT_OUTFILE),
23 | metafile: options.metafile ?? false,
24 | };
25 | };
26 |
27 | export const getIdByOptions = ({
28 | platform,
29 | dev,
30 | minify,
31 | }: Pick): number => {
32 | let value = OptionFlag.None; // = 0
33 |
34 | // platform
35 | value |= platform === 'android' ? OptionFlag.PlatformAndroid : 0;
36 | value |= platform === 'ios' ? OptionFlag.PlatformIos : 0;
37 | value |= platform === 'web' ? OptionFlag.PlatformWeb : 0;
38 |
39 | // dev & minify
40 | value |= dev ? OptionFlag.Dev : 0;
41 | value |= minify ? OptionFlag.Minify : 0;
42 |
43 | return value;
44 | };
45 |
46 | /**
47 | * For resolve environment mismatch issue.
48 | *
49 | * Run bundler with `--dev=false` (production), but babel's env is still using `development` environment.
50 | * In this case, run bundle after build it will be occurs unexpected errors.
51 | *
52 | * For eg. `react-native-reanimated` using babel plugin for transform.
53 | * Plugin follows babel environment, but main source(by esbuild) isn't.
54 | *
55 | * Override `NODE_ENV`, `BABEL_ENV` to bundler's environment.
56 | *
57 | * @see {@link https://github.com/babel/babel/blob/v7.23.0/packages/babel-core/src/config/helpers/environment.ts#L2}
58 | */
59 | export const setEnvironment = (isDev: boolean): void => {
60 | const env = isDev ? 'development' : 'production';
61 | process.env.NODE_ENV = env;
62 | process.env.BABEL_ENV = env;
63 | };
64 |
65 | export const ASSET_PATH = 'assets';
66 | export const PUBLIC_PATH = 'public';
67 | export const STATUS_CACHE_FILE = 'build-status.json';
68 |
69 | export const getDevServerPublicPath = (root: string): string => {
70 | return path.resolve(root, LOCAL_CACHE_DIR, PUBLIC_PATH);
71 | };
72 |
73 | export const getBuildStatusCachePath = (root: string): string => {
74 | return path.resolve(root, LOCAL_CACHE_DIR, STATUS_CACHE_FILE);
75 | };
76 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "private": true,
4 | "scripts": {
5 | "android": "react-native run-android",
6 | "ios": "react-native run-ios",
7 | "lint": "eslint .",
8 | "start": "rne start",
9 | "serve": "rne serve",
10 | "bundle:android": "rne bundle --platform=android --bundle-output=dist/android.jsbundle --assets-dest=dist/assets",
11 | "bundle:ios": "rne bundle --platform=ios --bundle-output=dist/ios.jsbundle --assets-dest=dist/assets",
12 | "bundle:web": "rne bundle --platform=web --bundle-output=public/index.js --entry-file=index.web.js",
13 | "start:metro": "react-native start",
14 | "bundle:metro:android": "react-native bundle --entry-file=index.js --platform=android --bundle-output=dist/android-metro.bundlejs",
15 | "bundle:metro:ios": "react-native bundle --entry-file=index.js --platform=ios --bundle-output=dist/ios-metro.bundlejs",
16 | "pod:install": "cd ios && pod install",
17 | "test": "jest"
18 | },
19 | "dependencies": {
20 | "@react-navigation/native": "^7.0.14",
21 | "@react-navigation/stack": "^7.1.0",
22 | "dripsy": "^4.3.8",
23 | "react": "18.3.1",
24 | "react-dom": "^18.3.1",
25 | "react-native": "0.76.5",
26 | "react-native-gesture-handler": "^2.21.2",
27 | "react-native-reanimated": "^3.16.5",
28 | "react-native-safe-area-context": "^5.0.0",
29 | "react-native-screens": "^4.3.0",
30 | "react-native-svg": "^15.10.1",
31 | "react-native-web": "^0.19.13"
32 | },
33 | "devDependencies": {
34 | "@babel/core": "^7.25.2",
35 | "@babel/plugin-transform-export-namespace-from": "^7.22.11",
36 | "@babel/plugin-transform-private-methods": "^7.22.5",
37 | "@babel/preset-env": "^7.25.3",
38 | "@babel/runtime": "^7.25.0",
39 | "@jest/globals": "^29.7.0",
40 | "@react-native-community/cli": "15.0.1",
41 | "@react-native-community/cli-platform-android": "15.0.1",
42 | "@react-native-community/cli-platform-ios": "15.0.1",
43 | "@react-native-esbuild/cli": "workspace:^",
44 | "@react-native-esbuild/jest": "workspace:^",
45 | "@react-native/babel-preset": "0.76.5",
46 | "@react-native/eslint-config": "0.76.5",
47 | "@react-native/metro-config": "0.76.5",
48 | "@react-native/typescript-config": "0.76.5",
49 | "@react-navigation/devtools": "^6.0.27",
50 | "@swc/jest": "^0.2.38",
51 | "@types/react": "^18.2.31",
52 | "@types/react-test-renderer": "^18.3.0",
53 | "babel-jest": "^29.7.0",
54 | "jest": "^29.7.0",
55 | "react-test-renderer": "18.2.0",
56 | "typescript": "^5.8.3"
57 | },
58 | "engines": {
59 | "node": ">=18"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/packages/transformer/lib/transformer/babel/babel.ts:
--------------------------------------------------------------------------------
1 | import {
2 | loadOptions,
3 | transformAsync,
4 | transformSync,
5 | transformFromAstAsync,
6 | transformFromAstSync,
7 | type TransformOptions,
8 | type Node,
9 | } from '@babel/core';
10 | import type {
11 | AsyncTransformer,
12 | SyncTransformer,
13 | TransformerContext,
14 | } from '../../types';
15 |
16 | const loadBabelOptions = (
17 | context: TransformerContext,
18 | options?: TransformOptions,
19 | ): ReturnType => {
20 | return loadOptions({
21 | ...options,
22 | root: context.root,
23 | filename: context.path,
24 | });
25 | };
26 |
27 | export const transformWithBabel: AsyncTransformer = async (
28 | source,
29 | context,
30 | preset,
31 | ) => {
32 | const babelOptions = loadBabelOptions(context, preset?.(context));
33 | if (!babelOptions) {
34 | throw new Error('cannot load babel options');
35 | }
36 |
37 | const result = await transformAsync(source, babelOptions);
38 | if (typeof result?.code !== 'string') {
39 | throw new Error('babel transformed source is empty');
40 | }
41 |
42 | return result.code;
43 | };
44 |
45 | export const transformSyncWithBabel: SyncTransformer = (
46 | source,
47 | context,
48 | preset,
49 | ) => {
50 | const babelOptions = loadBabelOptions(context, preset?.(context));
51 | if (!babelOptions) {
52 | throw new Error('cannot load babel options');
53 | }
54 |
55 | const result = transformSync(source, babelOptions);
56 | if (typeof result?.code !== 'string') {
57 | throw new Error('babel transformed source is empty');
58 | }
59 |
60 | return result.code;
61 | };
62 |
63 | export const transformWithBabelAst: AsyncTransformer<
64 | TransformOptions,
65 | Node
66 | > = async (ast, context, preset) => {
67 | const babelOptions = loadBabelOptions(context, preset?.(context));
68 | if (!babelOptions) {
69 | throw new Error('cannot load babel options');
70 | }
71 |
72 | const result = await transformFromAstAsync(ast, undefined, babelOptions);
73 | if (typeof result?.code !== 'string') {
74 | throw new Error('babel transformed source is empty');
75 | }
76 |
77 | return result.code;
78 | };
79 |
80 | export const transformSyncWithBabelAST: SyncTransformer<
81 | TransformOptions,
82 | Node
83 | > = (ast, context, preset) => {
84 | const babelOptions = loadBabelOptions(context, preset?.(context));
85 | if (!babelOptions) {
86 | throw new Error('cannot load babel options');
87 | }
88 |
89 | const result = transformFromAstSync(ast, undefined, babelOptions);
90 | if (typeof result?.code !== 'string') {
91 | throw new Error('babel transformed source is empty');
92 | }
93 |
94 | return result.code;
95 | };
96 |
--------------------------------------------------------------------------------
/example/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { makeTheme } from 'dripsy';
2 | import { CONTAINER_MAX_WIDTH, ROOT_FONT_SIZE } from '../constants';
3 | import { colors } from './colors';
4 |
5 | type Theme = typeof themeLight;
6 |
7 | declare module 'dripsy' {
8 | // eslint-disable-next-line @typescript-eslint/no-empty-interface -- allow
9 | interface DripsyCustomTheme extends Theme {}
10 | }
11 |
12 | const themeLight = makeTheme({
13 | colors,
14 | fontSizes: {
15 | $default: ROOT_FONT_SIZE, // 16
16 | $h1: ROOT_FONT_SIZE * 2, // 32
17 | $h2: ROOT_FONT_SIZE * 1.5, // 24
18 | $h3: ROOT_FONT_SIZE * 1.25, // 20
19 | $text: ROOT_FONT_SIZE, // 16
20 | $button: ROOT_FONT_SIZE * 1.25, // 20
21 | },
22 | text: {
23 | // Default text style
24 | body: {
25 | fontSize: ROOT_FONT_SIZE,
26 | },
27 | h1: {
28 | marginVertical: 0,
29 | fontSize: '$h1',
30 | fontWeight: 'bold',
31 | },
32 | h2: {
33 | marginVertical: 0,
34 | fontSize: '$h2',
35 | fontWeight: 'bold',
36 | },
37 | h3: {
38 | marginVertical: 0,
39 | fontSize: '$h3',
40 | fontWeight: 'bold',
41 | },
42 | p: {
43 | marginVertical: 0,
44 | fontSize: '$text',
45 | },
46 | button: {
47 | color: '$text_primary',
48 | fontWeight: 500,
49 | },
50 | primary: {
51 | color: '$text_primary',
52 | },
53 | secondary: {
54 | color: '$text_secondary',
55 | },
56 | danger: {
57 | color: '$danger',
58 | fontWeight: 700,
59 | },
60 | highlight: {
61 | backgroundColor: '$esbuild',
62 | color: '$black',
63 | fontWeight: 700,
64 | },
65 | },
66 | space: {
67 | $00: 0, // 0
68 | $01: ROOT_FONT_SIZE * 0.25, // 4
69 | $02: ROOT_FONT_SIZE * 0.5, // 8
70 | $03: ROOT_FONT_SIZE * 0.75, // 12
71 | $04: ROOT_FONT_SIZE, // 16
72 | $05: ROOT_FONT_SIZE * 1.25, // 20
73 | $06: ROOT_FONT_SIZE * 1.5, // 24
74 | $07: ROOT_FONT_SIZE * 2, // 32
75 | },
76 | radii: {
77 | $md: 8,
78 | $full: 9999,
79 | },
80 | sizes: {
81 | container: CONTAINER_MAX_WIDTH,
82 | },
83 | layout: {
84 | // base container style
85 | container: {
86 | flex: 1,
87 | px: '$04',
88 | backgroundColor: '$white',
89 | alignSelf: 'center',
90 | },
91 | center: {
92 | alignItems: 'center',
93 | justifyContent: 'center',
94 | },
95 | },
96 | types: {
97 | strictVariants: true,
98 | reactNativeTypesOnly: true,
99 | },
100 | });
101 |
102 | // TODO: Add dark theme
103 | const themeDark: Theme = {
104 | ...themeLight,
105 | };
106 |
107 | export { themeLight, themeDark };
108 |
--------------------------------------------------------------------------------
/packages/dev-server/lib/middlewares/__tests__/serveAsset.test.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage, ServerResponse } from 'node:http';
2 | import path from 'node:path';
3 | import fs from 'node:fs/promises';
4 | import type { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
5 | import { ASSET_PATH } from '@react-native-esbuild/config';
6 | import { faker } from '@faker-js/faker';
7 | import { createServeAssetMiddleware } from '../serveAsset';
8 | import type { DevServerMiddleware } from '../../types';
9 | import {
10 | getMockedRequest,
11 | getMockedResponse,
12 | getMockedFileHandler,
13 | } from './mocks';
14 |
15 | describe('serve-asset-middleware', () => {
16 | let middleware: DevServerMiddleware;
17 |
18 | beforeEach(() => {
19 | middleware = createServeAssetMiddleware({
20 | devServerOptions: {
21 | host: 'localhost',
22 | port: 8081,
23 | },
24 | bundler: {
25 | getRoot: jest.fn().mockReturnValue('/root'),
26 | } as unknown as ReactNativeEsbuildBundler,
27 | });
28 | });
29 |
30 | describe('when request url is empty', () => {
31 | let request: IncomingMessage;
32 | let response: ServerResponse;
33 | let next: jest.Mock;
34 |
35 | beforeEach(() => {
36 | request = getMockedRequest({ url: '' });
37 | response = getMockedResponse();
38 | next = jest.fn();
39 | middleware(request, response, next);
40 | });
41 |
42 | it('should skip', () => {
43 | expect(next).toBeCalledTimes(1);
44 | });
45 | });
46 |
47 | describe('when request url is valid asset url', () => {
48 | let assetRequestUrl: string;
49 | let request: IncomingMessage;
50 | let response: ServerResponse;
51 | let next: jest.Mock;
52 |
53 | beforeEach(() => {
54 | assetRequestUrl = path.join(ASSET_PATH, faker.system.fileName());
55 | request = getMockedRequest({ url: `/${assetRequestUrl}` });
56 | response = getMockedResponse();
57 | next = jest.fn();
58 | });
59 |
60 | describe('asset path is file', () => {
61 | let data: string;
62 | let size: number;
63 |
64 | beforeEach(() => {
65 | data = faker.string.alphanumeric(10);
66 | size = faker.number.int();
67 | jest.spyOn(fs, 'open').mockImplementation(() => {
68 | return Promise.resolve(
69 | getMockedFileHandler({
70 | data,
71 | size,
72 | isDirectory: false,
73 | }),
74 | );
75 | });
76 | middleware(request, response, next);
77 | });
78 |
79 | it('should response data with status 200', () => {
80 | expect(response.writeHead).toBeCalledWith(
81 | 200,
82 | expect.objectContaining({ 'Content-Length': size }),
83 | );
84 | expect(response.end).toBeCalledWith(data);
85 | });
86 | });
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # react-native-esbuild
4 |
5 |

6 |
7 | ⚡️ An extremely fast bundler + React Native
8 |
9 | [](https://esbuild.github.io)
10 | [](https://swc.rs)
11 | [](https://reactnative.dev)
12 |
13 | [](https://www.npmjs.com/settings/react-native-esbuild/packages)
14 | [](https://github.com/vercel/style-guide)
15 | [](https://www.typescriptlang.org)
16 |
17 |
18 |
19 | # Features
20 |
21 | 
22 |
23 | > [!WARNING]
24 | > This project is under development
25 |
26 | - ⚡️ Blazing Fast Build
27 | - 🌳 Supports Tree Shaking
28 | - 💾 In-memory & Local File System Caching
29 | - 🎨 Flexible & Extensible
30 | - 🔥 Supports JSC & Hermes Runtime
31 | - 🔄 Supports Live Reload
32 | - 🐛 Supports Debugging(Flipper, Chrome Debugger)
33 | - 🌍 Supports All Platforms(Android, iOS, Web)
34 | - ✨ New Architecture Ready
35 | - 🃏 Jest transformer included
36 |
37 | 👉 See a demo application built with a web target [here](https://rne-web-demo.vercel.app).
38 |
39 | # Getting Started
40 |
41 | Go to [documentation](https://react-native-esbuild.vercel.app)
42 |
43 | # Development
44 |
45 | ```bash
46 | # install dependencies and run build
47 | yarn && yarn build
48 |
49 | # in example directory,
50 | # run example application (start @react-native-esbuild/dev-server)
51 | yarn start
52 |
53 | # build example project (example/dist/)
54 | yarn build:android
55 | yarn build:ios
56 | ```
57 |
58 | ```bash
59 | # run yarn commands
60 | yarn workspace
61 |
62 | # build all packages
63 | yarn build
64 |
65 | # run all test
66 | yarn test
67 |
68 | # show project dependencies graph
69 | nx graph
70 | ```
71 |
72 | # Benchmark
73 |
74 | ```bash
75 | # in `example` directory
76 |
77 | # react-native-esbuild (with cache)
78 | time yarn bundle:android
79 | time yarn bundle:ios
80 | # react-native-esbuild (without cache)
81 | time yarn bundle:android --reset-cache
82 | time yarn bundle:ios --reset-cache
83 |
84 | # metro (with cache)
85 | time yarn bundle:metro:android
86 | time yarn bundle:metro:ios
87 | # metro (without cache)
88 | time yarn bundle:metro:android --reset-cache
89 | time yarn bundle:metro:ios --reset-cache
90 | ```
91 |
92 | # License
93 |
94 | [MIT](./LICENSE)
95 |
--------------------------------------------------------------------------------
/packages/dev-server/lib/middlewares/symbolicate.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'node:url';
2 | import {
3 | parseStackFromRawBody,
4 | symbolicateStackTrace,
5 | } from '@react-native-esbuild/symbolicate';
6 | import type { BundleOptions } from '@react-native-esbuild/config';
7 | import {
8 | parseBundleOptionsForWeb,
9 | parseBundleOptionsFromRequestUrl,
10 | type ParsedBundleOptions,
11 | } from '../helpers';
12 | import { logger } from '../shared';
13 | import type { DevServerMiddlewareCreator } from '../types';
14 |
15 | const TAG = 'symbolicate-middleware';
16 |
17 | export const createSymbolicateMiddleware: DevServerMiddlewareCreator<{
18 | webBundleOptions?: Partial;
19 | }> = (context, options) => {
20 | const bundler = context.bundler;
21 | const webBundleOptions = options?.webBundleOptions;
22 |
23 | return function symbolicateMiddleware(request, response, next) {
24 | if (!request.url) {
25 | logger.warn(`(${TAG}) request url is empty`);
26 | next();
27 | return;
28 | }
29 |
30 | const { pathname } = parse(request.url, true);
31 | if (!(pathname && pathname.endsWith('/symbolicate'))) {
32 | next();
33 | return;
34 | }
35 |
36 | try {
37 | const stack = parseStackFromRawBody(request.rawBody);
38 | const targetStack = stack.find(({ file }) => file.startsWith('http'));
39 | if (!targetStack) {
40 | throw new Error('unable to get symbolicate stack');
41 | }
42 |
43 | let bundleOptions: ParsedBundleOptions | null = null;
44 | if (webBundleOptions) {
45 | bundleOptions = parseBundleOptionsForWeb(
46 | webBundleOptions,
47 | 'sourcemap',
48 | ).bundleOptions;
49 | } else {
50 | bundleOptions = parseBundleOptionsFromRequestUrl(
51 | targetStack.file,
52 | ).bundleOptions;
53 | }
54 |
55 | if (!bundleOptions) {
56 | throw new Error('unable to parse bundle options');
57 | }
58 |
59 | bundler
60 | .getBundleResult(bundleOptions)
61 | .then(({ result, error }) => {
62 | if (error) throw error;
63 | return symbolicateStackTrace(result.sourcemap, stack).catch(
64 | (error) => {
65 | logger.debug('unable to symbolicate stack trace', {
66 | error: error as Error,
67 | });
68 | return { stack: [], codeFrame: null };
69 | },
70 | );
71 | })
72 | .then((symbolicateResult) => {
73 | response.writeHead(200).end(JSON.stringify(symbolicateResult));
74 | })
75 | .catch((error) => {
76 | logger.error('unable to get bundle', error as Error);
77 | response.writeHead(500).end();
78 | });
79 | } catch (error) {
80 | logger.warn('invalid symbolicate request', error as Error);
81 | response.writeHead(400).end();
82 | }
83 | };
84 | };
85 |
--------------------------------------------------------------------------------
/packages/transformer/lib/transformer/swc/presets.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Options,
3 | JscConfig,
4 | TsParserConfig,
5 | EsParserConfig,
6 | } from '@swc/core';
7 | import type {
8 | TransformerOptionsPreset,
9 | SwcJestPresetOptions,
10 | } from '../../types';
11 |
12 | const getParserOptions = (path: string): TsParserConfig | EsParserConfig => {
13 | return /\.tsx?$/.test(path)
14 | ? ({
15 | syntax: 'typescript',
16 | tsx: true,
17 | dynamicImport: true,
18 | } as TsParserConfig)
19 | : ({
20 | syntax: 'ecmascript',
21 | jsx: true,
22 | exportDefaultFrom: true,
23 | } as EsParserConfig);
24 | };
25 |
26 | /**
27 | * swc transform options preset for react-native runtime.
28 | */
29 | const getReactNativeRuntimePreset = (
30 | jscConfig?: Pick,
31 | ): TransformerOptionsPreset => {
32 | return (context) => ({
33 | minify: false,
34 | sourceMaps: false,
35 | isModule: true,
36 | inputSourceMap: false,
37 | inlineSourcesContent: false,
38 | jsc: {
39 | parser: getParserOptions(context.path),
40 | target: 'es5',
41 | loose: false,
42 | externalHelpers: true,
43 | keepClassNames: true,
44 | transform: jscConfig?.transform,
45 | experimental: jscConfig?.experimental,
46 | },
47 | filename: context.path,
48 | root: context.root,
49 | });
50 | };
51 |
52 | const getJestPreset = (
53 | options: SwcJestPresetOptions,
54 | ): TransformerOptionsPreset => {
55 | const plugins = [
56 | (options.experimental?.mutableCjsExports ?? true)
57 | ? ['swc_mut_cjs_exports', {}]
58 | : null,
59 | options.experimental?.customCoverageInstrumentation
60 | ? [
61 | 'swc-plugin-coverage-instrument',
62 | options.experimental.customCoverageInstrumentation,
63 | ]
64 | : null,
65 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- swc type
66 | ].filter(Boolean) as [string, Record][];
67 |
68 | return (context) => ({
69 | sourceMaps: 'inline',
70 | jsc: {
71 | parser: getParserOptions(context.path),
72 | target: 'es2022',
73 | transform: {
74 | /**
75 | * @see {@link https://github.com/swc-project/jest/blob/v0.2.29/index.ts#L119}
76 | */
77 | hidden: { jest: true },
78 | react: {
79 | runtime: 'automatic',
80 | development: context.dev,
81 | refresh: false,
82 | },
83 | },
84 | experimental: { plugins },
85 | },
86 | module: { type: options.module === 'esm' ? 'es6' : 'commonjs' },
87 | filename: context.path,
88 | root: context.root,
89 | });
90 | };
91 |
92 | const getMinifyPreset = () => {
93 | return () => ({ compress: true, mangle: true, sourceMap: false });
94 | };
95 |
96 | export { getReactNativeRuntimePreset, getJestPreset, getMinifyPreset };
97 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'jest';
2 |
3 | const transform = {
4 | '^.+\\.(t|j)sx?$': '@swc/jest',
5 | };
6 |
7 | const config: Config = {
8 | projects: [
9 | {
10 | displayName: '@react-native-esbuild/cli',
11 | transform,
12 | testEnvironment: 'node',
13 | testMatch: ['/packages/cli/**/*.test.ts'],
14 | setupFilesAfterEnv: ['/test/setup.ts'],
15 | },
16 | {
17 | displayName: '@react-native-esbuild/config',
18 | transform,
19 | testEnvironment: 'node',
20 | testMatch: ['/packages/config/**/*.test.ts'],
21 | setupFilesAfterEnv: ['/test/setup.ts'],
22 | },
23 | {
24 | displayName: '@react-native-esbuild/core',
25 | transform,
26 | testEnvironment: 'node',
27 | testMatch: ['/packages/core/**/*.test.ts'],
28 | setupFilesAfterEnv: ['/test/setup.ts'],
29 | },
30 | {
31 | displayName: '@react-native-esbuild/config',
32 | transform,
33 | testEnvironment: 'node',
34 | testMatch: ['/packages/config/**/*.test.ts'],
35 | setupFilesAfterEnv: ['/test/setup.ts'],
36 | },
37 | {
38 | displayName: '@react-native-esbuild/dev-server',
39 | transform,
40 | testEnvironment: 'node',
41 | testMatch: ['/packages/dev-server/**/*.test.ts'],
42 | setupFilesAfterEnv: ['/test/setup.ts'],
43 | },
44 | {
45 | displayName: '@react-native-esbuild/internal',
46 | transform,
47 | testEnvironment: 'node',
48 | testMatch: ['/packages/internal/**/*.test.ts'],
49 | setupFilesAfterEnv: ['/test/setup.ts'],
50 | },
51 | {
52 | displayName: '@react-native-esbuild/plugins',
53 | transform,
54 | testEnvironment: 'node',
55 | testMatch: ['/packages/plugins/**/*.test.ts'],
56 | setupFilesAfterEnv: ['/test/setup.ts'],
57 | },
58 | {
59 | displayName: '@react-native-esbuild/symbolicate',
60 | transform,
61 | testEnvironment: 'node',
62 | testMatch: ['/packages/symbolicate/**/*.test.ts'],
63 | setupFilesAfterEnv: ['/test/setup.ts'],
64 | },
65 | {
66 | displayName: '@react-native-esbuild/transformer',
67 | transform,
68 | testEnvironment: 'node',
69 | testMatch: ['/packages/transformer/**/*.test.ts'],
70 | setupFilesAfterEnv: ['/test/setup.ts'],
71 | },
72 | {
73 | displayName: '@react-native-esbuild/utils',
74 | transform,
75 | testEnvironment: 'node',
76 | testMatch: ['/packages/utils/**/*.test.ts'],
77 | setupFilesAfterEnv: ['/test/setup.ts'],
78 | },
79 | ],
80 | testPathIgnorePatterns: [
81 | 'node_modules/',
82 | 'docs/*',
83 | 'example/*',
84 | '**/__tests__/*',
85 | ],
86 | collectCoverageFrom: ['packages/*/lib/**/*'],
87 | };
88 |
89 | export default config;
90 |
--------------------------------------------------------------------------------