diff --git a/.eslintrc.js b/.eslintrc.js
index bf4642bb..158cadde 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -29,6 +29,7 @@ module.exports = {
rules: {
semi: ['error', 'always'],
quotes: ['error', 'single'],
+ camelcase: 'off',
'new-cap': 'off',
'object-curly-spacing': ['error', 'always'],
'array-bracket-spacing': 'off',
@@ -43,6 +44,10 @@ module.exports = {
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-throw-literal': 'off',
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
+ '@typescript-eslint/no-unsafe-member-access': 'off',
+ '@typescript-eslint/no-unsafe-assignment': 'off',
+ '@typescript-eslint/no-unsafe-argument': 'off',
+ '@typescript-eslint/no-non-null-assertion': 'off',
},
},
{
@@ -51,6 +56,15 @@ module.exports = {
'no-console': 'off',
},
},
+ {
+ files: ['packages/hmr/lib/runtime/*.ts'],
+ rules: {
+ 'no-console': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unsafe-call': 'off',
+ '@typescript-eslint/no-unsafe-argument': 'off',
+ },
+ },
{
files: ['packages/jest/lib/**/*.ts'],
rules: {
diff --git a/.yarn/cache/@types-md5-npm-2.3.5-bd5c825c57-a86baf0521.zip b/.yarn/cache/@types-md5-npm-2.3.5-bd5c825c57-a86baf0521.zip
new file mode 100644
index 00000000..bb83c3d1
Binary files /dev/null and b/.yarn/cache/@types-md5-npm-2.3.5-bd5c825c57-a86baf0521.zip differ
diff --git a/.yarn/cache/@types-react-refresh-npm-0.14.3-d201a15407-ff48c18f92.zip b/.yarn/cache/@types-react-refresh-npm-0.14.3-d201a15407-ff48c18f92.zip
new file mode 100644
index 00000000..6bf799ac
Binary files /dev/null and b/.yarn/cache/@types-react-refresh-npm-0.14.3-d201a15407-ff48c18f92.zip differ
diff --git a/.yarn/cache/esbuild-dependency-graph-npm-0.3.0-44c84f4894-831634605d.zip b/.yarn/cache/esbuild-dependency-graph-npm-0.3.0-44c84f4894-831634605d.zip
new file mode 100644
index 00000000..1378bdaa
Binary files /dev/null and b/.yarn/cache/esbuild-dependency-graph-npm-0.3.0-44c84f4894-831634605d.zip differ
diff --git a/.yarn/cache/esbuild-plugin-module-id-npm-0.1.3-fd4d7e8747-7ae6493884.zip b/.yarn/cache/esbuild-plugin-module-id-npm-0.1.3-fd4d7e8747-7ae6493884.zip
new file mode 100644
index 00000000..6e671624
Binary files /dev/null and b/.yarn/cache/esbuild-plugin-module-id-npm-0.1.3-fd4d7e8747-7ae6493884.zip differ
diff --git a/.yarn/cache/swc-plugin-global-module-npm-0.1.0-alpha.5-cd339b699a-10523c5464.zip b/.yarn/cache/swc-plugin-global-module-npm-0.1.0-alpha.5-cd339b699a-10523c5464.zip
new file mode 100644
index 00000000..49c0da61
Binary files /dev/null and b/.yarn/cache/swc-plugin-global-module-npm-0.1.0-alpha.5-cd339b699a-10523c5464.zip differ
diff --git a/README.md b/README.md
index 0f40b9ff..4168443b 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@
- 💾 In-memory & Local File System Caching
- 🎨 Flexible & Extensible
- 🔥 Supports JSC & Hermes Runtime
-- 🔄 Supports Live Reload
+- 🔄 Supports HMR & Live Reload
- 🐛 Supports Debugging(Flipper, Chrome Debugger)
- 🌍 Supports All Platforms(Android, iOS, Web)
- ✨ New Architecture Ready
diff --git a/docs/pages/configuration/basic-configuration.mdx b/docs/pages/configuration/basic-configuration.mdx
index 8c5d10b8..31755260 100644
--- a/docs/pages/configuration/basic-configuration.mdx
+++ b/docs/pages/configuration/basic-configuration.mdx
@@ -16,6 +16,9 @@ exports.default = {};
By default, follow the configuration below.
```js
+/**
+ * @type {import('@react-native-esbuild/core').Config}
+ */
exports.default = {
cache: true,
logger: {
@@ -28,13 +31,6 @@ exports.default = {
assetExtensions: [/* internal/lib/defaults.ts */],
},
transformer: {
- jsc: {
- transform: {
- react: {
- runtime: 'automatic',
- },
- },
- },
stripFlowPackageNames: ['react-native'],
},
web: {
@@ -68,7 +64,6 @@ Resolver configurations.
Transformer configurations.
-- `transformer.jsc`: [jsc](https://swc.rs/docs/configuration/compilation) config in swc.
- `transformer.stripFlowPackageNames`: Package names to strip flow syntax from (Defaults to `['react-native']`)
- `transformer.fullyTransformPackageNames`: Package names to fully transform with [metro-react-native-babel-preset](https://github.com/facebook/react-native/tree/main/packages/react-native-babel-preset) from
- `transformer.additionalTransformRules`: Additional transform rules. This rules will be applied before phase of transform to es5
@@ -90,6 +85,15 @@ Additional Esbuild plugins.
For more details, go to [Custom Plugins](/configuration/custom-plugins)
+### experimental
+
+
+ Experimental configurations.
+
+
+- `experimental.hmr`: Enable HMR(Hot Module Replacement) on development mode. (Defaults to `false`)
+ - For more details and limitations, go to [Hot Module Replacement](/limitations/hot-module-replacement).
+
## Types
@@ -210,9 +214,16 @@ interface Config {
*/
plugins?: EsbuildPlugin[];
/**
- * Client event receiver
+ * Experimental configurations
*/
- reporter?: (event: ReportableEvent) => void;
+ experimental?: {
+ /**
+ * Enable HMR(Hot Module Replacement) on development mode.
+ *
+ * Defaults to `false`.
+ */
+ hmr?: boolean;
+ };
}
```
@@ -239,28 +250,3 @@ type BabelTransformRule = TransformRuleBase;
```
-
-
-ReportableEvent
-
-```ts
-type ReportableEvent = ClientLogEvent;
-
-interface ClientLogEvent {
- type: 'client_log';
- level:
- | 'trace'
- | 'info'
- | 'warn'
- | 'error'
- | 'log'
- | 'group'
- | 'groupCollapsed'
- | 'groupEnd'
- | 'debug';
- data: unknown[];
- mode: 'BRIDGE' | 'NOBRIDGE';
-}
-```
-
-
diff --git a/docs/pages/getting-started/installation.md b/docs/pages/getting-started/installation.md
index 530dc5de..78e783a7 100644
--- a/docs/pages/getting-started/installation.md
+++ b/docs/pages/getting-started/installation.md
@@ -31,7 +31,7 @@ And create `react-native-esbuild.js` to project root.
exports.default = {};
```
-for more details, go to [Configuration](/configuration/basic).
+for more details, go to [Basic Configuration](/configuration/basic-configuration).
## Native Setup
diff --git a/docs/pages/limitations/hot-module-replacement.md b/docs/pages/limitations/hot-module-replacement.md
deleted file mode 100644
index 9cdb6133..00000000
--- a/docs/pages/limitations/hot-module-replacement.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Hot Module Replacement
-
-Esbuild doesn't currently support Hot Module Replacement(HMR).
-
-Metro is implementing HMR capabilities based on [react-refresh](https://www.npmjs.com/package/react-refresh). I'll be looking at working with this, but as it is one of the complex implementations, unfortunately not sure when it will be available.
diff --git a/docs/pages/limitations/hot-module-replacement.mdx b/docs/pages/limitations/hot-module-replacement.mdx
new file mode 100644
index 00000000..a494cd39
--- /dev/null
+++ b/docs/pages/limitations/hot-module-replacement.mdx
@@ -0,0 +1,30 @@
+import { Callout } from 'nextra/components'
+
+# Hot Module Replacement
+
+
+ HMR(Hot Module Replacement) is experimental.
+
+
+esbuild doesn't currently support Hot Module Replacement(HMR).
+
+So, I working hard for implement custom HMR and it's partially available as an experimental feature.
+
+You can enable HMR by `experimental.hmr` set to `true` in your configuration file.
+
+```js
+/**
+ * @type {import('@react-native-esbuild/core').Config}
+ */
+exports.default = {
+ // ...
+ experimental: {
+ hmr: true,
+ },
+};
+```
+
+and here are some limitations.
+
+- Detects changes in the `/*` only.
+- Changes detected in `/node_modules/*` will be ignored and fully refreshed after rebuild.
diff --git a/example/.gitignore b/example/.gitignore
index f4c3ff65..89fdaefa 100644
--- a/example/.gitignore
+++ b/example/.gitignore
@@ -73,6 +73,9 @@ yarn-error.log
!.yarn/sdks
!.yarn/versions
+# @swc
+.swc
+
# @react-native-esbuild
.rne
.swc
diff --git a/example/react-native-esbuild.config.js b/example/react-native-esbuild.config.js
index d5655063..9547c723 100644
--- a/example/react-native-esbuild.config.js
+++ b/example/react-native-esbuild.config.js
@@ -29,4 +29,7 @@ exports.default = {
],
},
},
+ experimental: {
+ hmr: true,
+ },
};
diff --git a/jest.config.ts b/jest.config.ts
index 097d9188..5da1e387 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -27,13 +27,6 @@ const config: Config = {
testMatch: ['/packages/core/**/*.test.ts'],
setupFilesAfterEnv: ['/test/setup.ts'],
},
- {
- displayName: '@react-native-esbuild/config',
- transform,
- testEnvironment: 'node',
- testMatch: ['/packages/config/**/*.test.ts'],
- setupFilesAfterEnv: ['/test/setup.ts'],
- },
{
displayName: '@react-native-esbuild/dev-server',
transform,
@@ -41,13 +34,6 @@ const config: Config = {
testMatch: ['/packages/dev-server/**/*.test.ts'],
setupFilesAfterEnv: ['/test/setup.ts'],
},
- {
- displayName: '@react-native-esbuild/internal',
- transform,
- testEnvironment: 'node',
- testMatch: ['/packages/internal/**/*.test.ts'],
- setupFilesAfterEnv: ['/test/setup.ts'],
- },
{
displayName: '@react-native-esbuild/plugins',
transform,
@@ -70,10 +56,10 @@ const config: Config = {
setupFilesAfterEnv: ['/test/setup.ts'],
},
{
- displayName: '@react-native-esbuild/utils',
+ displayName: '@react-native-esbuild/shared',
transform,
testEnvironment: 'node',
- testMatch: ['/packages/utils/**/*.test.ts'],
+ testMatch: ['/packages/shared/**/*.test.ts'],
setupFilesAfterEnv: ['/test/setup.ts'],
},
],
diff --git a/packages/cli/lib/cli.ts b/packages/cli/lib/cli.ts
index 86f2a34d..a2e3a783 100644
--- a/packages/cli/lib/cli.ts
+++ b/packages/cli/lib/cli.ts
@@ -3,8 +3,7 @@ import { hideBin } from 'yargs/helpers';
import {
DEFAULT_ENTRY_POINT,
DEFAULT_WEB_ENTRY_POINT,
-} from '@react-native-esbuild/config';
-import { VERSION } from './constants';
+} from '@react-native-esbuild/shared';
import type { RawArgv } from './types';
const commonOptions = {
@@ -27,7 +26,7 @@ const commonOptions = {
export const cli = (): RawArgv | Promise => {
return yargs(hideBin(process.argv))
.scriptName('rne')
- .version(VERSION)
+ .version(self._version as string)
.usage('$0 [args]')
.command(
'start',
diff --git a/packages/cli/lib/commands/bundle.ts b/packages/cli/lib/commands/bundle.ts
index 407a829c..e801dd06 100644
--- a/packages/cli/lib/commands/bundle.ts
+++ b/packages/cli/lib/commands/bundle.ts
@@ -1,12 +1,11 @@
import path from 'node:path';
import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
import {
- DEFAULT_ENTRY_POINT,
+ DEFAULT_WEB_ENTRY_POINT,
type BundleOptions,
-} from '@react-native-esbuild/config';
+} from '@react-native-esbuild/shared';
import { printDebugOptions } from '../helpers';
import { bundleArgvSchema } from '../schema';
-import { presets } from '../presets';
import { logger } from '../shared';
import type { Command } from '../types';
@@ -16,7 +15,7 @@ import type { Command } from '../types';
export const bundle: Command = async (argv) => {
const bundleArgv = bundleArgvSchema.parse(argv);
const bundleOptions: Partial = {
- entry: path.resolve(bundleArgv['entry-file'] ?? DEFAULT_ENTRY_POINT),
+ entry: path.resolve(bundleArgv['entry-file'] ?? DEFAULT_WEB_ENTRY_POINT),
sourcemap: bundleArgv['sourcemap-output'],
outfile: bundleArgv['bundle-output'],
assetsDir: bundleArgv['assets-dest'],
@@ -32,10 +31,6 @@ export const bundle: Command = async (argv) => {
printDebugOptions(bundleOptions);
const bundler = new ReactNativeEsbuildBundler(root);
- (bundleOptions.platform === 'web' ? presets.web : presets.native).forEach(
- bundler.addPlugin.bind(bundler),
- );
-
await bundler.initialize();
await bundler.bundle(bundleOptions);
};
diff --git a/packages/cli/lib/commands/serve.ts b/packages/cli/lib/commands/serve.ts
index b0771fe5..c4048930 100644
--- a/packages/cli/lib/commands/serve.ts
+++ b/packages/cli/lib/commands/serve.ts
@@ -1,8 +1,7 @@
import { ReactNativeWebServer } from '@react-native-esbuild/dev-server';
-import type { BundleOptions } from '@react-native-esbuild/config';
+import type { BundleOptions } from '@react-native-esbuild/shared';
import { printDebugOptions } from '../helpers';
import { serveArgvSchema } from '../schema';
-import { presets } from '../presets';
import { logger } from '../shared';
import type { Command } from '../types';
@@ -28,12 +27,7 @@ export const serve: Command = async (argv): Promise => {
logger.debug('bundle options');
printDebugOptions(bundleOptions);
- const server = await new ReactNativeWebServer(
- serveOptions,
- bundleOptions,
- ).initialize((bundler) => {
- presets.web.forEach(bundler.addPlugin.bind(bundler));
- });
-
+ const server = new ReactNativeWebServer(serveOptions, bundleOptions);
+ await server.initialize();
await server.listen();
};
diff --git a/packages/cli/lib/commands/start.ts b/packages/cli/lib/commands/start.ts
index d94f63b9..30d85a54 100644
--- a/packages/cli/lib/commands/start.ts
+++ b/packages/cli/lib/commands/start.ts
@@ -1,10 +1,9 @@
/* eslint-disable quotes -- Allow quote in template literal */
import path from 'node:path';
import { ReactNativeAppServer } from '@react-native-esbuild/dev-server';
-import { DEFAULT_ENTRY_POINT } from '@react-native-esbuild/config';
+import { DEFAULT_ENTRY_POINT } from '@react-native-esbuild/shared';
import { enableInteractiveMode, printDebugOptions } from '../helpers';
import { startArgvSchema } from '../schema';
-import { presets } from '../presets';
import { logger } from '../shared';
import type { Command } from '../types';
@@ -21,12 +20,8 @@ export const start: Command = async (argv) => {
logger.debug('start options');
printDebugOptions({ entry, ...serveOptions });
- const server = await new ReactNativeAppServer(serveOptions).initialize(
- (bundler) => {
- presets.native.forEach(bundler.addPlugin.bind(bundler));
- },
- );
-
+ const server = new ReactNativeAppServer(serveOptions);
+ await server.initialize();
await server.listen(() => {
if (
enableInteractiveMode((keyName) => {
diff --git a/packages/cli/lib/constants/index.ts b/packages/cli/lib/constants/index.ts
deleted file mode 100644
index ec782205..00000000
--- a/packages/cli/lib/constants/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import pkg from '../../package.json';
-
-export const VERSION = pkg.version;
diff --git a/packages/cli/lib/helpers/cli.ts b/packages/cli/lib/helpers/cli.ts
index d866da86..ff7c1cd5 100644
--- a/packages/cli/lib/helpers/cli.ts
+++ b/packages/cli/lib/helpers/cli.ts
@@ -1,4 +1,4 @@
-import { colors } from '@react-native-esbuild/utils';
+import { colors } from '@react-native-esbuild/shared';
import { logger } from '../shared';
export const getCommand = (
diff --git a/packages/cli/lib/index.ts b/packages/cli/lib/index.ts
index 385b54ae..aa4e633f 100644
--- a/packages/cli/lib/index.ts
+++ b/packages/cli/lib/index.ts
@@ -1,5 +1,5 @@
import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
-import { LogLevel } from '@react-native-esbuild/utils';
+import { LogLevel } from '@react-native-esbuild/shared';
import { cli } from './cli';
import * as Commands from './commands';
import { getCommand, handleUncaughtException } from './helpers';
diff --git a/packages/cli/lib/schema/index.ts b/packages/cli/lib/schema/index.ts
index 1902aad3..c609b31c 100644
--- a/packages/cli/lib/schema/index.ts
+++ b/packages/cli/lib/schema/index.ts
@@ -1,5 +1,5 @@
import path from 'node:path';
-import { SUPPORT_PLATFORMS } from '@react-native-esbuild/config';
+import { SUPPORT_PLATFORMS } from '@react-native-esbuild/shared';
import { z } from 'zod';
const resolvePath = (filepath: string): string =>
diff --git a/packages/cli/lib/shared.ts b/packages/cli/lib/shared.ts
index fbc4868f..4c11af6a 100644
--- a/packages/cli/lib/shared.ts
+++ b/packages/cli/lib/shared.ts
@@ -1,3 +1,3 @@
-import { Logger } from '@react-native-esbuild/utils';
+import { Logger } from '@react-native-esbuild/shared';
export const logger = new Logger('cli');
diff --git a/packages/cli/package.json b/packages/cli/package.json
index a0bd9a82..fa60817c 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -39,11 +39,10 @@
"url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
},
"dependencies": {
- "@react-native-esbuild/config": "workspace:*",
"@react-native-esbuild/core": "workspace:*",
"@react-native-esbuild/dev-server": "workspace:*",
"@react-native-esbuild/plugins": "workspace:*",
- "@react-native-esbuild/utils": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"yargs": "^17.7.2",
"zod": "^3.22.2"
},
diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md
deleted file mode 100644
index 2e9b70b6..00000000
--- a/packages/config/CHANGELOG.md
+++ /dev/null
@@ -1,344 +0,0 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-## [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)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [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)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [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)
-
-### Miscellaneous Chores
-
-- remove unused code and update comments ([7e03116](https://github.com/leegeunhyeok/react-native-esbuild/commit/7e03116882a9018fdeef1cd137bcc4b169d24d54))
-- update prepack script ([7c155dd](https://github.com/leegeunhyeok/react-native-esbuild/commit/7c155dd1190b3909112895bed8e2fbc916559b6f))
-
-### Code Refactoring
-
-- import orders ([26d4e45](https://github.com/leegeunhyeok/react-native-esbuild/commit/26d4e454abb89b1b7d2e0eadaf15b27b124a34b5))
-
-## [0.1.0-beta.9](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.8...v0.1.0-beta.9) (2023-10-22)
-
-### Code Refactoring
-
-- **dev-server:** now read assets from origin path ([64e75df](https://github.com/leegeunhyeok/react-native-esbuild/commit/64e75df281e32e549c51a0f544c5c8ae2779fe92))
-- move transformer options to each tranform module ([5d8cc9b](https://github.com/leegeunhyeok/react-native-esbuild/commit/5d8cc9ba0e870e47cbbd4d8591f1bc643df1f25c))
-
-### Build System
-
-- **deps:** bump version up transform packages ([205d3ff](https://github.com/leegeunhyeok/react-native-esbuild/commit/205d3ff2dc0c8df62e3d0ddfce2576e726256c94))
-
-## [0.1.0-beta.8](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.7...v0.1.0-beta.8) (2023-10-10)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [0.1.0-beta.7](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.6...v0.1.0-beta.7) (2023-10-10)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [0.1.0-beta.6](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.5...v0.1.0-beta.6) (2023-10-09)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [0.1.0-beta.5](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.4...v0.1.0-beta.5) (2023-10-08)
-
-### Bug Fixes
-
-- default entry file path ([1e96aee](https://github.com/leegeunhyeok/react-native-esbuild/commit/1e96aee7542232d3bc829bccbecb5e2ec4e834fa))
-
-### Miscellaneous Chores
-
-- fix keywords ([83c635d](https://github.com/leegeunhyeok/react-native-esbuild/commit/83c635d2e0cf0570513bf6b5a25b816ded976abc))
-
-## [0.1.0-beta.4](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.3...v0.1.0-beta.4) (2023-10-08)
-
-### Features
-
-- web support ([1723b0c](https://github.com/leegeunhyeok/react-native-esbuild/commit/1723b0c6b30e1c1bb1f5781cc11a093822a60f3d)), closes [#36](https://github.com/leegeunhyeok/react-native-esbuild/issues/36)
-
-## [0.1.0-beta.3](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.2...v0.1.0-beta.3) (2023-10-04)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [0.1.0-beta.2](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.1...v0.1.0-beta.2) (2023-10-04)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [0.1.0-beta.1](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-beta.0...v0.1.0-beta.1) (2023-10-03)
-
-### Features
-
-- add getBuildStatusCachePath ([57dd3b4](https://github.com/leegeunhyeok/react-native-esbuild/commit/57dd3b4b2cb1a9a046241c7b61c2f09f72385851))
-- improve logging ([7f93e19](https://github.com/leegeunhyeok/react-native-esbuild/commit/7f93e19a82dbadd80529356041a126698e99bcac)), closes [#26](https://github.com/leegeunhyeok/react-native-esbuild/issues/26)
-
-### Code Refactoring
-
-- move extension constants to internal package ([be450bd](https://github.com/leegeunhyeok/react-native-esbuild/commit/be450bdfda652aa380f8873cc0b8fcc824551ad0))
-- rename config types to options ([7512fae](https://github.com/leegeunhyeok/react-native-esbuild/commit/7512faeb3f7a19365bbf7f9c2ed929e7abe4f538))
-- root based local cache directory ([b30e324](https://github.com/leegeunhyeok/react-native-esbuild/commit/b30e32423cf626dcaed123f4b1d55abdc726e677))
-
-## [0.1.0-beta.0](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.39...v0.1.0-beta.0) (2023-09-25)
-
-### Miscellaneous Chores
-
-- add comments to setEnvironment ([c9a0f7e](https://github.com/leegeunhyeok/react-native-esbuild/commit/c9a0f7e3f75fbc5548f1566c8c1b636f23fb30cf))
-
-### Code Refactoring
-
-- change function declaration to arrow function ([9aeae13](https://github.com/leegeunhyeok/react-native-esbuild/commit/9aeae1368cdfde8d998b85ebfd609be13b05a50f))
-
-### Build System
-
-- set packages as external ([dd4417f](https://github.com/leegeunhyeok/react-native-esbuild/commit/dd4417fe07c7bd87357246914743067343fdeccb)), closes [#22](https://github.com/leegeunhyeok/react-native-esbuild/issues/22)
-
-## [0.1.0-alpha.39](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.38...v0.1.0-alpha.39) (2023-09-24)
-
-### Code Refactoring
-
-- move bundler config types to core pacakge ([0924b6f](https://github.com/leegeunhyeok/react-native-esbuild/commit/0924b6f04fe59c538d18d5f49abaedc3a61df61d))
-
-## [0.1.0-alpha.38](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.37...v0.1.0-alpha.38) (2023-09-24)
-
-### Features
-
-- improve configurations ([79c9a68](https://github.com/leegeunhyeok/react-native-esbuild/commit/79c9a687b63ed52244e2dc2f4a7a50f6e5983afd))
-
-### Code Refactoring
-
-- cli option types ([4d7c346](https://github.com/leegeunhyeok/react-native-esbuild/commit/4d7c3468ccb509215164bc29cf5c6d0c265b67f1))
-
-## [0.1.0-alpha.37](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.36...v0.1.0-alpha.37) (2023-09-23)
-
-### Bug Fixes
-
-- **plugins:** some issue on resolve platform specified assets ([562b3ec](https://github.com/leegeunhyeok/react-native-esbuild/commit/562b3ec651eb6cb1c68f4714cdf9817d42e114fa))
-
-## [0.1.0-alpha.33](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.32...v0.1.0-alpha.33) (2023-09-22)
-
-### Code Refactoring
-
-- update bundle config type ([274558e](https://github.com/leegeunhyeok/react-native-esbuild/commit/274558e958bf8c4b04abb58df5473d3470b38026))
-
-## [0.1.0-alpha.31](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.30...v0.1.0-alpha.31) (2023-09-19)
-
-### Features
-
-- add --metafile option for esbuild metafile ([a696fab](https://github.com/leegeunhyeok/react-native-esbuild/commit/a696fabe44e122fb866ca92d7a2518f0fd23cc0c)), closes [#8](https://github.com/leegeunhyeok/react-native-esbuild/issues/8)
-
-### Bug Fixes
-
-- unwrap iife for hermes optimization ([220233f](https://github.com/leegeunhyeok/react-native-esbuild/commit/220233f9afc738b6ddb4b2ac0edaac5f4f499632)), closes [#5](https://github.com/leegeunhyeok/react-native-esbuild/issues/5)
-
-### Miscellaneous Chores
-
-- bump version up packages ([b0c87fd](https://github.com/leegeunhyeok/react-native-esbuild/commit/b0c87fd8694b6c725267d66494d761809da27111))
-- remove unused module ([11738fe](https://github.com/leegeunhyeok/react-native-esbuild/commit/11738fe371230c35898768a90406bfd69fcc2fb3))
-
-### Code Refactoring
-
-- apply eslint rules ([4792d4a](https://github.com/leegeunhyeok/react-native-esbuild/commit/4792d4a1662ad87fa052b93c709703a8d5f6fe46))
-
-## [0.1.0-alpha.30](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.29...v0.1.0-alpha.30) (2023-08-23)
-
-**Note:** Version bump only for package @react-native-esbuild/config
-
-## [0.1.0-alpha.29](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.28...v0.1.0-alpha.29) (2023-08-17)
-
-### Features
-
-- inject react native polyfills only once ([5b54909](https://github.com/leegeunhyeok/react-native-esbuild/commit/5b5490943ba7a7ea9bdda6d76281b3752224e36d))
-- specify global object by platform ([8abf893](https://github.com/leegeunhyeok/react-native-esbuild/commit/8abf893bde650808bcee201ed6b40081ffa6136f))
-
-### Code Refactoring
-
-- add reactNativeInternal ([f553bdf](https://github.com/leegeunhyeok/react-native-esbuild/commit/f553bdff0d95b9e9240e66ab18bc82c9deaf33e2))
-
-### Miscellaneous Chores
-
-- bump version up pacakges ([e235610](https://github.com/leegeunhyeok/react-native-esbuild/commit/e235610379fbf8f5c6978ecded5dbe6549834975))
-
-## [0.1.0-alpha.26](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.25...v0.1.0-alpha.26) (2023-08-10)
-
-### Code Refactoring
-
-- rename bitwiseOptions to getIdByOptions ([8055d6a](https://github.com/leegeunhyeok/react-native-esbuild/commit/8055d6a32e1f716615bd91385931ee99b5cf0d83))
-
-## [0.1.0-alpha.22](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.21...v0.1.0-alpha.22) (2023-08-09)
-
-### Features
-
-- add root ([acde580](https://github.com/leegeunhyeok/react-native-esbuild/commit/acde580db75bffd27e5c12ea11d483bc585ea87a))
-- add root option to transformers ([68c8c52](https://github.com/leegeunhyeok/react-native-esbuild/commit/68c8c524daa458fad5d5f060ffcaba3ca40b2344))
-- add setEnvironment ([2372662](https://github.com/leegeunhyeok/react-native-esbuild/commit/237266258553547e7638d6b499aa44e40f33e37f))
-- cleanup esbuild options ([7ff4cd5](https://github.com/leegeunhyeok/react-native-esbuild/commit/7ff4cd5d08bb66db964945976218b459dc3dae96))
-- follow @react-native-community/cli options ([723a0fd](https://github.com/leegeunhyeok/react-native-esbuild/commit/723a0fda5f4c462c7d1bda0afc084ed48a5b7d3e))
-
-## [0.1.0-alpha.21](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.20...v0.1.0-alpha.21) (2023-08-08)
-
-### Features
-
-- disable swc loose option ([ab1da4d](https://github.com/leegeunhyeok/react-native-esbuild/commit/ab1da4d8fbfbc9028e1de074e9df1d9dee96ff24))
-
-### Bug Fixes
-
-- wrong **DEV** value ([620c9b4](https://github.com/leegeunhyeok/react-native-esbuild/commit/620c9b4c40d3d97f5676a5114e19c586e06738fb))
-
-## [0.1.0-alpha.19](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.18...v0.1.0-alpha.19) (2023-08-06)
-
-### Features
-
-- **core:** support platform scoped bundle ([1a7094b](https://github.com/leegeunhyeok/react-native-esbuild/commit/1a7094b51c1327fff6708f32638a78c078a74914))
-
-### Performance Improvements
-
-- improve transform performance ([42670f2](https://github.com/leegeunhyeok/react-native-esbuild/commit/42670f2bfd4d82df623d45713012ccc21bb8678e))
-
-## [0.1.0-alpha.18](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.17...v0.1.0-alpha.18) (2023-08-05)
-
-### Features
-
-- add bitwiseOptions ([786191d](https://github.com/leegeunhyeok/react-native-esbuild/commit/786191df504bba61c71685196e82d2b2ba4e268d))
-- add scoped cache system ([8d1f0bd](https://github.com/leegeunhyeok/react-native-esbuild/commit/8d1f0bd3235f977a73f1f3725ce393fae244cf97))
-
-### Miscellaneous Chores
-
-- add cleanup script ([0f03232](https://github.com/leegeunhyeok/react-native-esbuild/commit/0f032326ad5a412942b77f40130d38a3efeff472))
-
-### Code Refactoring
-
-- improve config types ([4bacba6](https://github.com/leegeunhyeok/react-native-esbuild/commit/4bacba65c9609191490d89b488a9e00d3127ef38))
-- separate config modules ([ce6d02d](https://github.com/leegeunhyeok/react-native-esbuild/commit/ce6d02d5c5e597469e75c8c6864b553afd53b501))
-
-## [0.1.0-alpha.17](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.16...v0.1.0-alpha.17) (2023-08-04)
-
-### Code Refactoring
-
-- remove cache option and now following dev option ([0bd385a](https://github.com/leegeunhyeok/react-native-esbuild/commit/0bd385a5931ddc69e258415d7f876bb96b6185de))
-
-## [0.1.0-alpha.16](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.15...v0.1.0-alpha.16) (2023-08-04)
-
-### Features
-
-- add transform options ([018a731](https://github.com/leegeunhyeok/react-native-esbuild/commit/018a7312679bfed118e6d26ffede696b293f4cb7))
-
-## [0.1.0-alpha.15](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.14...v0.1.0-alpha.15) (2023-08-04)
-
-### Features
-
-- improve esbuild log ([fa23610](https://github.com/leegeunhyeok/react-native-esbuild/commit/fa23610b9eed876974c8dc07e90baabe405b1df1))
-
-### Miscellaneous Chores
-
-- add rimraf for cleanup build directories ([13356fe](https://github.com/leegeunhyeok/react-native-esbuild/commit/13356fec1868b7634da86bca522e987b5bee2284))
-
-## [0.1.0-alpha.14](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.13...v0.1.0-alpha.14) (2023-08-04)
-
-### Features
-
-- add svg-transform-plugin ([0526207](https://github.com/leegeunhyeok/react-native-esbuild/commit/05262075d33d8df24a392e731a418435cf74c2bd))
-
-## [0.1.0-alpha.12](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.11...v0.1.0-alpha.12) (2023-08-03)
-
-### Code Refactoring
-
-- add registerPlugins ([263219f](https://github.com/leegeunhyeok/react-native-esbuild/commit/263219f629b8535a1928e3ef5e87dc0ce797fe9d))
-- **core:** move build-status-plugin to core ([7d23543](https://github.com/leegeunhyeok/react-native-esbuild/commit/7d2354325cdd52b014aecaaa327071300877a1fc))
-
-## [0.1.0-alpha.11](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.10...v0.1.0-alpha.11) (2023-08-03)
-
-### Features
-
-- copying assets when build complete ([db10be1](https://github.com/leegeunhyeok/react-native-esbuild/commit/db10be14be375910835def9efd07bf7e3efe6398))
-
-### Bug Fixes
-
-- **core:** change react native initialize order ([81b5a30](https://github.com/leegeunhyeok/react-native-esbuild/commit/81b5a3033d0f478dea69a20b2922b0e7bf736858))
-
-## [0.1.0-alpha.10](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.9...v0.1.0-alpha.10) (2023-08-01)
-
-### Features
-
-- **plugins:** implement asset-register-plugin ([9237cb4](https://github.com/leegeunhyeok/react-native-esbuild/commit/9237cb4802ffe4d9c2696292e6a63d276a1f44e1))
-
-## [0.1.0-alpha.9](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.8...v0.1.0-alpha.9) (2023-07-31)
-
-### Features
-
-- add core options ([3743760](https://github.com/leegeunhyeok/react-native-esbuild/commit/3743760d285b7e55db1cc634b53800be36c05d1d))
-- change assetsDest to assetsDir ([2ec231b](https://github.com/leegeunhyeok/react-native-esbuild/commit/2ec231b7a63ee68f0acb9c16fba5dea6f355b62a))
-- improve configs and implement start command ([936d33b](https://github.com/leegeunhyeok/react-native-esbuild/commit/936d33b2f916c22410aa7241ae53b634f83116ee))
-- improve module resolution for react native polyfills ([300df3f](https://github.com/leegeunhyeok/react-native-esbuild/commit/300df3f0c6654764ed9539d13243346faa6559a9))
-
-### Performance Improvements
-
-- improve bundle performance ([72844d5](https://github.com/leegeunhyeok/react-native-esbuild/commit/72844d5b5d5529b1245a1642218b5ef9d41e3dd5))
-
-### Code Refactoring
-
-- cleanup import statement ([badc372](https://github.com/leegeunhyeok/react-native-esbuild/commit/badc372d6db1ddb8f3b68270829ea4be842c3c63))
-- split config modules to each target ([f37427d](https://github.com/leegeunhyeok/react-native-esbuild/commit/f37427d3160b7eb995befbeea8116fe53cb9e1d5))
-
-## [0.1.0-alpha.8](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.7...v0.1.0-alpha.8) (2023-07-29)
-
-### Reverts
-
-- Revert "chore: change type to module" ([96c32ee](https://github.com/leegeunhyeok/react-native-esbuild/commit/96c32ee767cb0553b0bbe0ba3c631da3dbc308bf))
-
-## [0.1.0-alpha.7](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.6...v0.1.0-alpha.7) (2023-07-29)
-
-### Miscellaneous Chores
-
-- change type to module ([6d63e8a](https://github.com/leegeunhyeok/react-native-esbuild/commit/6d63e8af31f4e485247add463142d81f86c0c0b2))
-
-## [0.1.0-alpha.5](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.4...v0.1.0-alpha.5) (2023-07-29)
-
-### Bug Fixes
-
-- **config:** add missed esbuild options ([b1fda0d](https://github.com/leegeunhyeok/react-native-esbuild/commit/b1fda0d6e92186a3853b3c71b5687c35b13fd2e8))
-
-## [0.1.0-alpha.3](https://github.com/leegeunhyeok/react-native-esbuild/compare/v0.1.0-alpha.2...v0.1.0-alpha.3) (2023-07-29)
-
-### Features
-
-- **config:** add outfile to esbuild config ([638e6a2](https://github.com/leegeunhyeok/react-native-esbuild/commit/638e6a27c1f48c5d3ab76bfb63cdc13682d92842))
-
-### Code Refactoring
-
-- **config:** improve config types ([1c2c170](https://github.com/leegeunhyeok/react-native-esbuild/commit/1c2c170d01c2beb2018ac745daaa3973a4368103))
-
-## 0.1.0-alpha.1 (2023-07-29)
-
-### Features
-
-- add base configs for build ([3acf916](https://github.com/leegeunhyeok/react-native-esbuild/commit/3acf91623d33e9d1f8ee48568d66e57d329683ec))
-- add sourcemap option ([bfb6c9e](https://github.com/leegeunhyeok/react-native-esbuild/commit/bfb6c9edc2338aa612e4f687b05d72e94bc70877))
-- **core:** add request bundle option ([5a76eca](https://github.com/leegeunhyeok/react-native-esbuild/commit/5a76ecac1e07211c95ec356e5829bb0f671009c9))
-
-### Bug Fixes
-
-- circular dependency ([f764fe5](https://github.com/leegeunhyeok/react-native-esbuild/commit/f764fe51c4ec31efd8c89826200bbe275f956e86))
-- process exit when error occurred ([a0ef5ab](https://github.com/leegeunhyeok/react-native-esbuild/commit/a0ef5ab055cab1828fe763473992d995bc65e23d))
-- set react-native as external module ([add4a20](https://github.com/leegeunhyeok/react-native-esbuild/commit/add4a20a3de08c26d42f39afab20c1a890a9939b))
-
-### Build System
-
-- add esbuild scripts ([b38b2c0](https://github.com/leegeunhyeok/react-native-esbuild/commit/b38b2c06bf7f8594fd17675c8d23e38a7f1678fb))
-- change base build config ([752e15a](https://github.com/leegeunhyeok/react-native-esbuild/commit/752e15af5560c6f5648344a2695257e819045d95))
-
-### Code Refactoring
-
-- change custom option variable names ([a0060dc](https://github.com/leegeunhyeok/react-native-esbuild/commit/a0060dcd3a59dc2899cbda90980c5c3aeb38de18))
-- **config:** change swc config builder name ([da39399](https://github.com/leegeunhyeok/react-native-esbuild/commit/da39399595b0a686316146c2d91ec0c5c6ad5bdc))
-- **config:** improve swc option builder ([6dc328a](https://github.com/leegeunhyeok/react-native-esbuild/commit/6dc328a6693edcb58d2a29dd401a4814430fb014))
-
-### Miscellaneous Chores
-
-- add dist directory to publish files ([1abbee2](https://github.com/leegeunhyeok/react-native-esbuild/commit/1abbee2dd1560ac7166903362c220263cd5d895a))
-- add packages ([a2076de](https://github.com/leegeunhyeok/react-native-esbuild/commit/a2076def60774fb9b39cfe90f5af35b44148a46f))
-- add prepack scripts ([3baa83b](https://github.com/leegeunhyeok/react-native-esbuild/commit/3baa83b9ce539c7c797a959a829aaf0e95d0d6d2))
-- update tsconfig for type declaration ([7458d94](https://github.com/leegeunhyeok/react-native-esbuild/commit/7458d945fb3e8c3a5a7b29a00eda197556a5fa5d))
diff --git a/packages/config/README.md b/packages/config/README.md
deleted file mode 100644
index fbcb19f3..00000000
--- a/packages/config/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# `@react-native-esbuild/config`
-
-> Shared configs for @react-native-esbuild/config
diff --git a/packages/config/lib/common/__tests__/__snapshots__/common.test.ts.snap b/packages/config/lib/common/__tests__/__snapshots__/common.test.ts.snap
deleted file mode 100644
index f1edf0b5..00000000
--- a/packages/config/lib/common/__tests__/__snapshots__/common.test.ts.snap
+++ /dev/null
@@ -1,3 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`getBuildStatusCachePath should match snapshot 1`] = `"/root/.rne/build-status.json"`;
diff --git a/packages/config/lib/common/__tests__/common.test.ts b/packages/config/lib/common/__tests__/common.test.ts
deleted file mode 100644
index 53c8c252..00000000
--- a/packages/config/lib/common/__tests__/common.test.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { getIdByOptions, getBuildStatusCachePath } from '../core';
-import { OptionFlag } from '../../types';
-
-const BASE_OPTIONS = {
- outfile: '',
- entry: '',
- metafile: false,
-} as const;
-
-const ROOT_DIR = '/root';
-
-describe.each([
- [
- { platform: 'android', dev: false, minify: false },
- OptionFlag.PlatformAndroid,
- ],
- [
- { platform: 'android', dev: true, minify: false },
- OptionFlag.PlatformAndroid | OptionFlag.Dev,
- ],
- [
- { platform: 'android', dev: false, minify: true },
- OptionFlag.PlatformAndroid | OptionFlag.Minify,
- ],
- [
- { platform: 'android', dev: true, minify: true },
- OptionFlag.PlatformAndroid | OptionFlag.Dev | OptionFlag.Minify,
- ],
- [{ platform: 'ios', dev: false, minify: false }, OptionFlag.PlatformIos],
- [
- { platform: 'ios', dev: true, minify: false },
- OptionFlag.PlatformIos | OptionFlag.Dev,
- ],
- [
- { platform: 'ios', dev: false, minify: true },
- OptionFlag.PlatformIos | OptionFlag.Minify,
- ],
- [
- { platform: 'ios', dev: true, minify: true },
- OptionFlag.PlatformIos | OptionFlag.Dev | OptionFlag.Minify,
- ],
- [{ platform: 'web', dev: false, minify: false }, OptionFlag.PlatformWeb],
- [
- { platform: 'web', dev: true, minify: false },
- OptionFlag.PlatformWeb | OptionFlag.Dev,
- ],
- [
- { platform: 'web', dev: false, minify: true },
- OptionFlag.PlatformWeb | OptionFlag.Minify,
- ],
- [
- { platform: 'web', dev: true, minify: true },
- OptionFlag.PlatformWeb | OptionFlag.Dev | OptionFlag.Minify,
- ],
-] as const)('getIdByOptions', (options, expected) => {
- const dev = options.dev ? 'true' : 'false';
- const minify = options.minify ? 'true' : 'false';
-
- describe(`platform: ${options.platform}, dev: ${dev}, minify: ${minify}`, () => {
- it(`should bitwise value is ${expected}`, () => {
- expect(getIdByOptions({ ...BASE_OPTIONS, ...options })).toEqual(expected);
- });
- });
-});
-
-describe('getBuildStatusCachePath', () => {
- it('should match snapshot', () => {
- expect(getBuildStatusCachePath(ROOT_DIR)).toMatchSnapshot();
- });
-});
diff --git a/packages/config/lib/common/index.ts b/packages/config/lib/common/index.ts
deleted file mode 100644
index 7877787b..00000000
--- a/packages/config/lib/common/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './core';
-export * from '../shares';
diff --git a/packages/config/lib/index.ts b/packages/config/lib/index.ts
deleted file mode 100644
index de4fec05..00000000
--- a/packages/config/lib/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './common';
-export * from './shares';
-export { OptionFlag } from './types';
-export type * from './types';
diff --git a/packages/config/lib/shares.ts b/packages/config/lib/shares.ts
deleted file mode 100644
index 8cbe1b2f..00000000
--- a/packages/config/lib/shares.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export const DEFAULT_ENTRY_POINT = 'index.js';
-export const DEFAULT_WEB_ENTRY_POINT = 'index.web.js';
-export const DEFAULT_OUTFILE = 'main.jsbundle';
-
-export const GLOBAL_CACHE_DIR = 'react-native-esbuild';
-export const LOCAL_CACHE_DIR = '.rne';
-
-export const SUPPORT_PLATFORMS = ['android', 'ios', 'web'] as const;
diff --git a/packages/config/lib/types.ts b/packages/config/lib/types.ts
deleted file mode 100644
index 52d4f1c3..00000000
--- a/packages/config/lib/types.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export type BundlerSupportPlatform = 'android' | 'ios' | 'web';
-
-export interface BundleOptions {
- platform: BundlerSupportPlatform;
- entry: string;
- outfile: string;
- dev: boolean;
- minify: boolean;
- metafile: boolean;
- sourcemap?: string;
- assetsDir?: string;
-}
-
-/**
- * Flags for `BundleOptions`
- */
-export enum OptionFlag {
- None = 0b00000000,
- PlatformAndroid = 0b00000001,
- PlatformIos = 0b00000010,
- PlatformWeb = 0b00000100,
- Dev = 0b00001000,
- Minify = 0b00010000,
-}
diff --git a/packages/core/lib/bundler/bundler.ts b/packages/core/lib/bundler/ReactNativeEsbuildBundler.ts
similarity index 56%
rename from packages/core/lib/bundler/bundler.ts
rename to packages/core/lib/bundler/ReactNativeEsbuildBundler.ts
index 5146daa3..dcfafc0d 100644
--- a/packages/core/lib/bundler/bundler.ts
+++ b/packages/core/lib/bundler/ReactNativeEsbuildBundler.ts
@@ -1,4 +1,5 @@
import path from 'node:path';
+import os from 'node:os';
import esbuild, {
type BuildOptions,
type BuildResult,
@@ -6,52 +7,71 @@ import esbuild, {
} from 'esbuild';
import invariant from 'invariant';
import ora from 'ora';
-import { getGlobalVariables } from '@react-native-esbuild/internal';
+import { isHMRBoundary } from '@react-native-esbuild/hmr';
import {
+ Logger,
+ LogLevel,
setEnvironment,
- combineWithDefaultBundleOptions,
getIdByOptions,
getDevServerPublicPath,
+ combineWithDefaultBundleOptions,
+ printLogo,
+ printVersion,
+ BuildMode,
+ type Id,
+ type BuildStatus,
+ type BuildContext,
+ type BuildStatusListener,
type BundleOptions,
-} from '@react-native-esbuild/config';
-import { Logger, LogLevel } from '@react-native-esbuild/utils';
-import { FileSystemWatcher } from '../watcher';
+ type Config,
+ type AdditionalData,
+ type PluginFactory,
+} from '@react-native-esbuild/shared';
+import {
+ getGlobalVariables,
+ type ClientLogEvent,
+} from '@react-native-esbuild/internal';
+import {
+ createBuildStatusPlugin,
+ createMetafilePlugin,
+} from '@react-native-esbuild/plugins';
import { logger } from '../shared';
-import type {
- Config,
- BundlerInitializeOptions,
- BuildTask,
- BuildStatus,
- BundleMode,
- BundlerAdditionalData,
- BundleResult,
- BundleRequestOptions,
- PluginContext,
- ReportableEvent,
- ReactNativeEsbuildPluginCreator,
-} from '../types';
-import { CacheStorage, SharedStorage } from './storages';
-import { createBuildStatusPlugin, createMetafilePlugin } from './plugins';
-import { BundlerEventEmitter } from './events';
import {
loadConfig,
+ createBuildTaskDelegate,
getConfigFromGlobal,
- createPromiseHandler,
getTransformedPreludeScript,
getResolveExtensionsOption,
getLoaderOption,
getEsbuildWebConfig,
-} from './helpers';
-import { printLogo, printVersion } from './logo';
+ getExternalFromPackageJson,
+ getExternalModulePattern,
+} from '../helpers';
+import type {
+ BundlerInitializeOptions,
+ BundleResult,
+ BundleRequestOptions,
+ BuildTask,
+ FileSystemWatchEventListener,
+} from '../types';
+import { FileSystemWatcher } from './watcher';
+import { CacheStorage } from './cache';
+import { ModuleManager } from './modules';
+import { BundlerEventEmitter } from './events';
+import { presets } from './plugins';
-export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
- public static caches = new CacheStorage();
- public static shared = new SharedStorage();
+export class ReactNativeEsbuildBundler
+ extends BundlerEventEmitter
+ implements BuildStatusListener, FileSystemWatchEventListener
+{
private appLogger = new Logger('app', LogLevel.Trace);
- private buildTasks = new Map();
- private plugins: ReactNativeEsbuildPluginCreator[] = [];
- private initialized = false;
+ private buildTasks = new Map();
+ private plugins: PluginFactory[] = [];
+ private watcher: FileSystemWatcher;
private config: Config;
+ private external: string[];
+ private externalPattern: string;
+ private initialized = false;
/**
* Must be bootstrapped first at the entry point
@@ -91,25 +111,29 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
}
public static async resetCache(): Promise {
- await ReactNativeEsbuildBundler.caches.clearAll();
+ await CacheStorage.clearAll();
logger.info('transform cache was reset');
}
constructor(private root: string = process.cwd()) {
super();
this.config = getConfigFromGlobal();
+ this.external = getExternalFromPackageJson(root);
+ this.externalPattern = getExternalModulePattern(
+ this.external,
+ this.config.resolver?.assetExtensions ?? [],
+ );
this.on('report', (event) => {
this.broadcastToReporter(event);
});
}
- private broadcastToReporter(event: ReportableEvent): void {
- // default reporter (for logging)
+ private broadcastToReporter(event: ClientLogEvent): void {
switch (event.type) {
case 'client_log': {
if (event.level === 'group' || event.level === 'groupCollapsed') {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument -- Allow any type.
- this.appLogger.group(...(event.data as any[]));
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- allow
+ this.appLogger.group(...(event.data as unknown as any[]));
return;
} else if (event.level === 'groupEnd') {
this.appLogger.groupEnd();
@@ -124,43 +148,25 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
break;
}
}
-
- // send event to custom reporter
- this.config.reporter?.(event);
- }
-
- private startWatcher(): Promise {
- return FileSystemWatcher.getInstance()
- .setHandler((event, changedFile, stats) => {
- const hasTask = this.buildTasks.size > 0;
- ReactNativeEsbuildBundler.shared.setValue({
- watcher: {
- changed: hasTask && event === 'change' ? changedFile : null,
- stats,
- },
- });
-
- for (const { context, handler } of this.buildTasks.values()) {
- context.rebuild().catch((error) => handler?.rejecter?.(error));
- }
- })
- .watch(this.root);
}
- private async getBuildOptionsForBundler(
- mode: BundleMode,
+ private async getBuildOptions(
+ mode: BuildMode,
bundleOptions: BundleOptions,
- additionalData?: BundlerAdditionalData,
+ additionalData?: AdditionalData,
): Promise {
const config = this.config;
invariant(config.resolver, 'invalid resolver configuration');
invariant(config.resolver.mainFields, 'invalid mainFields');
invariant(config.transformer, 'invalid transformer configuration');
- invariant(config.resolver.assetExtensions, 'invalid assetExtension');
+ invariant(config.resolver.assetExtensions, 'invalid assetExtensions');
invariant(config.resolver.sourceExtensions, 'invalid sourceExtensions');
setEnvironment(bundleOptions.dev);
+ const hmrEnabled = Boolean(
+ mode === BuildMode.Watch && bundleOptions.dev && config.experimental?.hmr,
+ );
const webSpecifiedOptions =
bundleOptions.platform === 'web'
? getEsbuildWebConfig(mode, this.root, bundleOptions)
@@ -171,19 +177,25 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
webSpecifiedOptions.outfile ?? path.basename(bundleOptions.entry);
}
- const context: PluginContext = {
- ...bundleOptions,
- id: this.identifyTaskByBundleOptions(bundleOptions),
+ const context: BuildContext = {
+ id: getIdByOptions(bundleOptions),
root: this.root,
config: this.config,
+ bundleOptions,
mode,
- additionalData,
+ moduleManager: new ModuleManager(),
+ cacheStorage: new CacheStorage(),
+ hmrEnabled,
+ additionalData: {
+ ...additionalData,
+ externalPattern: this.externalPattern,
+ },
};
return {
entryPoints: [bundleOptions.entry],
outfile: bundleOptions.outfile,
- sourceRoot: path.dirname(bundleOptions.entry),
+ sourceRoot: this.root,
mainFields: config.resolver.mainFields,
resolveExtensions: getResolveExtensionsOption(
bundleOptions,
@@ -191,23 +203,30 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
config.resolver.assetExtensions,
),
loader: getLoaderOption(config.resolver.assetExtensions),
- define: getGlobalVariables(bundleOptions),
+ define: getGlobalVariables(bundleOptions.dev, bundleOptions.platform),
banner: {
- js: await getTransformedPreludeScript(bundleOptions, this.root),
+ js: await getTransformedPreludeScript(
+ bundleOptions,
+ this.root,
+ [hmrEnabled ? 'swc-plugin-global-module/runtime' : undefined].filter(
+ Boolean,
+ ) as string[],
+ ),
},
plugins: [
- createBuildStatusPlugin(context, {
- onStart: this.handleBuildStart.bind(this),
- onUpdate: this.handleBuildStateUpdate.bind(this),
- onEnd: this.handleBuildEnd.bind(this),
- }),
+ // Common plugins
+ createBuildStatusPlugin(context, {}),
createMetafilePlugin(context),
- // Added plugin creators.
+ // Added plugin creators
+ ...(bundleOptions.platform === 'web'
+ ? presets.web
+ : presets.native
+ ).map((createPlugin) => createPlugin(context)),
...this.plugins.map((plugin) => plugin(context)),
// Additional plugins in configuration.
...(config.plugins ?? []),
],
- legalComments: bundleOptions.dev ? 'inline' : 'none',
+ legalComments: 'none',
target: 'es6',
format: 'esm',
supported: {
@@ -225,52 +244,105 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
logLevel: 'silent',
bundle: true,
sourcemap: true,
+ metafile: true,
minify: bundleOptions.minify,
- metafile: bundleOptions.metafile,
- write: mode === 'bundle',
+ write: mode === BuildMode.Bundle,
...webSpecifiedOptions,
};
}
- private identifyTaskByBundleOptions(bundleOptions: BundleOptions): number {
- return getIdByOptions(bundleOptions);
- }
-
private throwIfNotInitialized(): void {
if (this.initialized) return;
throw new Error('bundler not initialized');
}
- private handleBuildStart(context: PluginContext): void {
+ private async setupTask(
+ bundleOptions: BundleOptions,
+ additionalData?: AdditionalData,
+ ): Promise {
+ const id = getIdByOptions(bundleOptions);
+ if (this.buildTasks.has(id)) {
+ return this.buildTasks.get(id)!;
+ }
+
+ const buildOptions = await this.getBuildOptions(
+ BuildMode.Watch,
+ bundleOptions,
+ additionalData,
+ );
+
+ const buildTask: BuildTask = {
+ esbuild: await esbuild.context(buildOptions),
+ delegate: createBuildTaskDelegate(),
+ buildCount: 0,
+ status: 'pending',
+ };
+
+ this.buildTasks.set(id, buildTask);
+ logger.debug(`new build context registered (id: ${id})`);
+
+ return buildTask;
+ }
+
+ private resetTask(context: BuildContext): void {
+ // Skip when bundle mode because task does not exist in this mode.
+ if (context.mode === BuildMode.Bundle) return;
+
+ const buildContext = this.buildTasks.get(context.id);
+ invariant(buildContext, 'no build context');
+
+ if (buildContext.buildCount === 0) return;
+
+ logger.debug(`reset build context (id: ${context.id})`, {
+ buildCount: buildContext.buildCount,
+ });
+
+ buildContext.delegate = createBuildTaskDelegate();
+ buildContext.status = 'pending';
+ buildContext.buildCount += 1;
+ }
+
+ public onBuildStart(context: BuildContext): void {
this.resetTask(context);
this.emit('build-start', { id: context.id });
}
- private handleBuildStateUpdate(
- buildState: BuildStatus,
- context: PluginContext,
- ): void {
- this.emit('build-status-change', { id: context.id, ...buildState });
+ public onBuild(context: BuildContext, status: BuildStatus): void {
+ this.emit('build-status-change', { id: context.id, ...status });
}
- private handleBuildEnd(
+ public onBuildEnd(
+ context: BuildContext,
data: { result: BuildResult; success: boolean },
- context: PluginContext,
): void {
/**
* Exit at the end of a build in bundle mode.
*
* If the build fails, exit with status 1.
*/
- if (context.mode === 'bundle') {
+ if (context.mode === BuildMode.Bundle) {
if (data.success) return;
process.exit(1);
}
- const currentTask = this.buildTasks.get(context.id);
- invariant(currentTask, 'no task');
+ invariant(data.success, 'build failed');
+ invariant(data.result.metafile, 'invalid metafile');
+ invariant(data.result.outputFiles, 'empty outputFiles');
+
+ const buildContext = this.buildTasks.get(context.id);
+ invariant(buildContext, 'no build context');
+
+ // if (context.hmrEnabled) {
+ // ReactNativeEsbuildBundler.sharedData.get(context.id).set({
+ // dependencyGraph: generateDependencyGraph(
+ // data.result.metafile,
+ // context.bundleOptions.entry,
+ // ),
+ // });
+ // }
+
const bundleEndedAt = new Date();
- const bundleFilename = context.outfile;
+ const bundleFilename = context.bundleOptions.outfile;
const bundleSourcemapFilename = `${bundleFilename}.map`;
const revisionId = bundleEndedAt.getTime().toString();
const { outputFiles } = data.result;
@@ -283,9 +355,6 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
};
try {
- invariant(data.success, 'build failed');
- invariant(outputFiles, 'empty outputFiles');
-
const bundleOutput = outputFiles.find(findFromOutputFile(bundleFilename));
const bundleSourcemapOutput = outputFiles.find(
findFromOutputFile(bundleSourcemapFilename),
@@ -293,7 +362,7 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
invariant(bundleOutput, 'empty bundle output');
invariant(bundleSourcemapOutput, 'empty sourcemap output');
- currentTask.handler?.resolver?.({
+ buildContext.delegate.success({
result: {
source: bundleOutput.contents,
sourcemap: bundleSourcemapOutput.contents,
@@ -303,74 +372,45 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
error: null,
});
} catch (error) {
- currentTask.handler?.rejecter?.(error);
+ buildContext.delegate.failure(error);
} finally {
- currentTask.status = 'resolved';
+ buildContext.status = 'resolved';
this.emit('build-end', {
revisionId,
id: context.id,
additionalData: context.additionalData,
+ update: null,
});
}
}
- private async getOrCreateBundleTask(
- bundleOptions: BundleOptions,
- additionalData?: BundlerAdditionalData,
- ): Promise {
- const targetTaskId = this.identifyTaskByBundleOptions(bundleOptions);
-
- if (!this.buildTasks.has(targetTaskId)) {
- logger.debug(`bundle task not registered (id: ${targetTaskId})`);
- const buildOptions = await this.getBuildOptionsForBundler(
- 'watch',
- bundleOptions,
- additionalData,
- );
- const handler = createPromiseHandler();
- const context = await esbuild.context(buildOptions);
- this.buildTasks.set(targetTaskId, {
- context,
- handler,
- status: 'pending',
- buildCount: 0,
+ public onFileSystemChange(event: string, path: string): void {
+ const hasBuildTask = this.buildTasks.size > 0;
+ const isChanged = event === 'change';
+
+ if (!(hasBuildTask && isChanged)) return;
+
+ // Set status as stale (need to rebuild when receive bundle requests)
+ this.buildTasks.forEach((task) => (task.status = 'pending'));
+
+ if (this.config.experimental?.hmr && isHMRBoundary(path)) {
+ // for (const [
+ // id,
+ // hmrController,
+ // ] of ReactNativeEsbuildBundler.hmr.entries()) {
+ // hmrController.getDelta(changedFile).then((update) => {
+ // this.emit('build-end', {
+ // id,
+ // update,
+ // revisionId: new Date().getTime().toString(),
+ // });
+ // });
+ // }
+ } else {
+ this.buildTasks.forEach(({ esbuild }) => {
+ esbuild.rebuild();
});
- // Trigger first build.
- context.rebuild().catch((error) => handler.rejecter?.(error));
- logger.debug(`bundle task is now watching (id: ${targetTaskId})`);
}
-
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Already `set()` if not exist.
- return this.buildTasks.get(targetTaskId)!;
- }
-
- private resetTask(context: PluginContext): void {
- // Skip when bundle mode because task does not exist in this mode.
- if (context.mode === 'bundle') return;
-
- const targetTask = this.buildTasks.get(context.id);
- invariant(targetTask, 'no task');
-
- logger.debug(`reset task (id: ${context.id})`, {
- buildCount: targetTask.buildCount,
- });
-
- this.buildTasks.set(context.id, {
- // Use created esbuild context.
- context: targetTask.context,
- /**
- * Set status to `pending` and create new handler when it is stale.
- *
- * - `buildCount` is 0, it is first build.
- * - `buildCount` is over 0, triggered rebuild (handler is stale).
- */
- handler:
- targetTask.buildCount === 0
- ? targetTask.handler
- : createPromiseHandler(),
- status: 'pending',
- buildCount: targetTask.buildCount + 1,
- });
}
public async initialize(options?: BundlerInitializeOptions): Promise {
@@ -385,7 +425,8 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
);
if (options?.watcherEnabled) {
- await this.startWatcher();
+ this.watcher = new FileSystemWatcher(this);
+ await this.watcher.watch(this.root);
}
this.initialized = true;
@@ -399,18 +440,18 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
return this;
}
- public addPlugin(creator: ReactNativeEsbuildPluginCreator): this {
+ public addPlugin(creator: PluginFactory): this {
this.plugins.push(creator);
return this;
}
public async bundle(
bundleOptions: Partial,
- additionalData?: BundlerAdditionalData,
+ additionalData?: AdditionalData,
): Promise {
this.throwIfNotInitialized();
- const buildOptions = await this.getBuildOptionsForBundler(
- 'bundle',
+ const buildOptions = await this.getBuildOptions(
+ BuildMode.Bundle,
combineWithDefaultBundleOptions(bundleOptions),
additionalData,
);
@@ -419,36 +460,44 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
public async serve(
bundleOptions: Partial,
- additionalData?: BundlerAdditionalData,
+ additionalData?: AdditionalData,
): Promise {
this.throwIfNotInitialized();
if (bundleOptions.platform !== 'web') {
throw new Error('serve mode is only available on web platform');
}
- const buildTask = await this.getOrCreateBundleTask(
+ const buildContext = await this.setupTask(
combineWithDefaultBundleOptions(bundleOptions),
additionalData,
);
- invariant(buildTask.handler, 'no handler');
- return buildTask.context.serve({
+ if (buildContext.status === 'pending') {
+ buildContext.esbuild.rebuild();
+ }
+
+ return buildContext.esbuild.serve({
servedir: getDevServerPublicPath(this.root),
});
}
public async getBundleResult(
bundleOptions: BundleRequestOptions,
- additionalData?: BundlerAdditionalData,
+ additionalData?: AdditionalData,
): Promise {
this.throwIfNotInitialized();
- const buildTask = await this.getOrCreateBundleTask(
+ const buildContext = await this.setupTask(
combineWithDefaultBundleOptions(bundleOptions),
additionalData,
);
- invariant(buildTask.handler, 'no handler');
- return buildTask.handler.task;
+ if (buildContext.status === 'pending') {
+ buildContext.esbuild.rebuild();
+ }
+
+ const result = await buildContext.delegate.promise;
+
+ return result;
}
public getRoot(): string {
diff --git a/packages/core/lib/bundler/cache/CacheController.ts b/packages/core/lib/bundler/cache/CacheController.ts
deleted file mode 100644
index 4241eae4..00000000
--- a/packages/core/lib/bundler/cache/CacheController.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import fs from 'node:fs';
-import path from 'node:path';
-import type { Cache } from '../../types';
-
-const OPTIONS = { encoding: 'utf-8' } as const;
-
-export class CacheController {
- private cache: Record = {};
-
- constructor(private cacheDirectory: string) {
- try {
- fs.accessSync(cacheDirectory, fs.constants.R_OK | fs.constants.W_OK);
- } catch (_error) {
- fs.mkdirSync(cacheDirectory, { recursive: true });
- }
- }
-
- public readFromMemory(key: string): Cache | undefined {
- return this.cache[key];
- }
-
- public readFromFileSystem(hash: string): Promise {
- return fs.promises
- .readFile(path.join(this.cacheDirectory, hash), OPTIONS)
- .catch(() => null);
- }
-
- public writeToMemory(key: string, cacheData: Cache): void {
- this.cache[key] = cacheData;
- }
-
- public writeToFileSystem(hash: string, data: string): Promise {
- return fs.promises
- .writeFile(path.join(this.cacheDirectory, hash), data, OPTIONS)
- .catch(() => void 0);
- }
-
- public reset(): void {
- this.cache = {};
- }
-}
diff --git a/packages/core/lib/bundler/cache/CacheStorageImpl.ts b/packages/core/lib/bundler/cache/CacheStorageImpl.ts
new file mode 100644
index 00000000..f905683b
--- /dev/null
+++ b/packages/core/lib/bundler/cache/CacheStorageImpl.ts
@@ -0,0 +1,51 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import {
+ GLOBAL_CACHE_DIR,
+ type Cache,
+ type CacheStorage,
+} from '@react-native-esbuild/shared';
+
+const OPTIONS = { encoding: 'utf-8' } as const;
+
+export class CacheStorageImpl implements CacheStorage {
+ private cache: Record = {};
+
+ public static clearAll(): Promise {
+ return fs.promises.rm(GLOBAL_CACHE_DIR, {
+ recursive: true,
+ });
+ }
+
+ constructor() {
+ try {
+ fs.accessSync(GLOBAL_CACHE_DIR, fs.constants.R_OK | fs.constants.W_OK);
+ } catch (_error) {
+ fs.mkdirSync(GLOBAL_CACHE_DIR, { recursive: true });
+ }
+ }
+
+ public readFromMemory(key: string): Cache | undefined {
+ return this.cache[key];
+ }
+
+ public readFromFileSystem(key: string): Promise {
+ return fs.promises
+ .readFile(path.join(GLOBAL_CACHE_DIR, key), OPTIONS)
+ .catch(() => undefined);
+ }
+
+ public writeToMemory(key: string, cacheData: Cache): void {
+ this.cache[key] = cacheData;
+ }
+
+ public writeToFileSystem(key: string, data: string): Promise {
+ return fs.promises
+ .writeFile(path.join(GLOBAL_CACHE_DIR, key), data, OPTIONS)
+ .catch(() => void 0);
+ }
+
+ public reset(): void {
+ this.cache = {};
+ }
+}
diff --git a/packages/core/lib/bundler/cache/__tests__/cache.test.ts b/packages/core/lib/bundler/cache/__tests__/cache.test.ts
index 4e6e3fd9..3b3ecf50 100644
--- a/packages/core/lib/bundler/cache/__tests__/cache.test.ts
+++ b/packages/core/lib/bundler/cache/__tests__/cache.test.ts
@@ -1,11 +1,10 @@
import fs from 'node:fs';
import { faker } from '@faker-js/faker';
-import { CacheController } from '../CacheController';
-import type { Cache } from '../../../types';
+import type { Cache } from '@react-native-esbuild/shared';
+import { CacheStorageImpl } from '../CacheStorageImpl';
-describe('CacheController', () => {
- const CACHE_DIR = '/cache';
- let manager: CacheController;
+describe('CacheStorageImpl', () => {
+ let storage: CacheStorageImpl;
let mockedFs: Record;
beforeAll(() => {
@@ -25,7 +24,7 @@ describe('CacheController', () => {
});
jest.spyOn(fs.promises, 'rm').mockReturnValue(Promise.resolve());
- manager = new CacheController(CACHE_DIR);
+ storage = new CacheStorageImpl();
mockedFs = {};
});
@@ -33,24 +32,6 @@ describe('CacheController', () => {
jest.restoreAllMocks();
});
- describe('getCacheDirectory', () => {
- it('should match snapshot', () => {
- expect(CACHE_DIR).toMatchSnapshot();
- });
-
- describe('when filename is present', () => {
- let filename: string;
-
- beforeEach(() => {
- filename = faker.system.fileName();
- });
-
- it('should join path with cache directory', () => {
- expect(`${CACHE_DIR}/${filename}`).toMatch(new RegExp(`/${filename}$`));
- });
- });
- });
-
describe('memory caching', () => {
describe('when write cache to memory', () => {
let cacheKey: string;
@@ -60,13 +41,13 @@ describe('CacheController', () => {
cacheKey = faker.string.uuid();
cache = {
data: faker.string.alphanumeric(10),
- modifiedAt: faker.date.past().getTime(),
+ mtimeMs: faker.date.past().getTime(),
};
- manager.writeToMemory(cacheKey, cache);
+ storage.writeToMemory(cacheKey, cache);
});
it('should cache data store to memory', () => {
- expect(manager.readFromMemory(cacheKey)).toMatchObject(cache);
+ expect(storage.readFromMemory(cacheKey)).toMatchObject(cache);
});
describe('when write cache with exist key to memory', () => {
@@ -74,12 +55,12 @@ describe('CacheController', () => {
beforeEach(() => {
otherData = faker.string.alphanumeric(10);
- manager.writeToMemory(cacheKey, { ...cache, data: otherData });
+ storage.writeToMemory(cacheKey, { ...cache, data: otherData });
});
it('should overwrite exist cache data', () => {
- expect(manager.readFromMemory(cacheKey)).not.toMatchObject(cache);
- expect(manager.readFromMemory(cacheKey)).toMatchObject({
+ expect(storage.readFromMemory(cacheKey)).not.toMatchObject(cache);
+ expect(storage.readFromMemory(cacheKey)).toMatchObject({
...cache,
data: otherData,
});
@@ -95,12 +76,12 @@ describe('CacheController', () => {
beforeEach(async () => {
hash = faker.string.alphanumeric(10);
data = faker.string.alphanumeric(10);
- await manager.writeToFileSystem(hash, data);
+ await storage.writeToFileSystem(hash, data);
});
describe('when write data to file system', () => {
it('should store data to file system', async () => {
- expect(await manager.readFromFileSystem(hash)).toEqual(data);
+ expect(await storage.readFromFileSystem(hash)).toEqual(data);
});
});
});
diff --git a/packages/core/lib/bundler/cache/index.ts b/packages/core/lib/bundler/cache/index.ts
index 33c5958a..05575d3e 100644
--- a/packages/core/lib/bundler/cache/index.ts
+++ b/packages/core/lib/bundler/cache/index.ts
@@ -1 +1 @@
-export * from './CacheController';
+export { CacheStorageImpl as CacheStorage } from './CacheStorageImpl';
diff --git a/packages/core/lib/bundler/events/index.ts b/packages/core/lib/bundler/events/index.ts
index cd3718b7..57b81931 100644
--- a/packages/core/lib/bundler/events/index.ts
+++ b/packages/core/lib/bundler/events/index.ts
@@ -1,9 +1,7 @@
import EventEmitter from 'node:events';
-import type {
- BundlerAdditionalData,
- BuildStatus,
- ReportableEvent,
-} from '../../types';
+import type { HMRTransformResult } from '@react-native-esbuild/hmr';
+import type { AdditionalData, BuildStatus } from '@react-native-esbuild/shared';
+import type { ClientLogEvent } from '@react-native-esbuild/internal';
export class BundlerEventEmitter extends EventEmitter {
addListener: (
@@ -41,16 +39,17 @@ export type BundlerEventListener = (
export interface BundlerEventPayload {
'build-start': {
id: number;
- additionalData?: BundlerAdditionalData;
+ additionalData?: AdditionalData;
};
'build-end': {
id: number;
revisionId: string;
- additionalData?: BundlerAdditionalData;
+ update: HMRTransformResult | null;
+ additionalData?: AdditionalData;
};
'build-status-change': BuildStatus & {
id: number;
- additionalData?: BundlerAdditionalData;
+ additionalData?: AdditionalData;
};
- report: ReportableEvent;
+ report: ClientLogEvent;
}
diff --git a/packages/core/lib/bundler/helpers/async.ts b/packages/core/lib/bundler/helpers/async.ts
deleted file mode 100644
index 0f8b7606..00000000
--- a/packages/core/lib/bundler/helpers/async.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { BundleResult, PromiseHandler } from '../../types';
-
-export const createPromiseHandler = (): PromiseHandler => {
- let resolver: PromiseHandler['resolver'] | undefined;
- let rejecter: PromiseHandler['rejecter'] | undefined;
-
- const task = new Promise((resolve, _reject) => {
- resolver = resolve;
- rejecter = (reason: Error) => {
- resolve({ result: null, error: reason });
- };
- });
-
- return { task, resolver, rejecter };
-};
diff --git a/packages/core/lib/bundler/index.ts b/packages/core/lib/bundler/index.ts
index a8145c19..a93a7e73 100644
--- a/packages/core/lib/bundler/index.ts
+++ b/packages/core/lib/bundler/index.ts
@@ -1,2 +1 @@
-export { ReactNativeEsbuildBundler } from './bundler';
-export type * from './events';
+export * from './ReactNativeEsbuildBundler';
diff --git a/packages/core/lib/bundler/modules/ModuleManagerImpl.ts b/packages/core/lib/bundler/modules/ModuleManagerImpl.ts
new file mode 100644
index 00000000..cae876fb
--- /dev/null
+++ b/packages/core/lib/bundler/modules/ModuleManagerImpl.ts
@@ -0,0 +1,26 @@
+import type { ModuleManager, ModuleId } from '@react-native-esbuild/shared';
+
+type ModuleIds = Record;
+
+export class ModuleManagerImpl implements ModuleManager {
+ // Entry point module id is always 0.
+ private static ENTRY_POINT_MODULE_ID = 0;
+ private INTERNAL_id = ModuleManagerImpl.ENTRY_POINT_MODULE_ID + 1;
+ private INTERNAL_moduleIds: ModuleIds = {};
+
+ public getModuleId(modulePath: string, isEntryPoint?: boolean): ModuleId {
+ // Already generated.
+ if (typeof this.INTERNAL_moduleIds[modulePath] === 'number') {
+ return this.INTERNAL_moduleIds[modulePath];
+ }
+
+ // Generate new module id.
+ return (this.INTERNAL_moduleIds[modulePath] = isEntryPoint
+ ? ModuleManagerImpl.ENTRY_POINT_MODULE_ID
+ : this.INTERNAL_id++);
+ }
+
+ public getModuleIds(): ModuleIds {
+ return this.INTERNAL_moduleIds;
+ }
+}
diff --git a/packages/core/lib/bundler/modules/dependencyGraph.ts b/packages/core/lib/bundler/modules/dependencyGraph.ts
new file mode 100644
index 00000000..0ee5b67d
--- /dev/null
+++ b/packages/core/lib/bundler/modules/dependencyGraph.ts
@@ -0,0 +1,9 @@
+import { DependencyGraph } from 'esbuild-dependency-graph';
+import type { Metafile } from 'esbuild';
+
+export const generateDependencyGraph = (
+ metafile: Metafile,
+ entryPoint: string,
+): DependencyGraph => {
+ return new DependencyGraph(metafile, entryPoint);
+};
diff --git a/packages/core/lib/bundler/modules/index.ts b/packages/core/lib/bundler/modules/index.ts
new file mode 100644
index 00000000..f58bdcde
--- /dev/null
+++ b/packages/core/lib/bundler/modules/index.ts
@@ -0,0 +1,2 @@
+export { ModuleManagerImpl as ModuleManager } from './ModuleManagerImpl';
+export * from './dependencyGraph';
diff --git a/packages/core/lib/bundler/plugins/index.ts b/packages/core/lib/bundler/plugins/index.ts
index 393e30c9..a9d72701 100644
--- a/packages/core/lib/bundler/plugins/index.ts
+++ b/packages/core/lib/bundler/plugins/index.ts
@@ -1,2 +1 @@
-export { createMetafilePlugin } from './metafilePlugin';
-export { createBuildStatusPlugin } from './statusPlugin';
+export * as presets from './presets';
diff --git a/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts b/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts
deleted file mode 100644
index 70bb8c1b..00000000
--- a/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import fs from 'node:fs/promises';
-import path from 'node:path';
-import type { BuildResult } from 'esbuild';
-import { logger } from '../../../shared';
-import type { ReactNativeEsbuildPluginCreator } from '../../../types';
-
-const NAME = 'metafile-plugin';
-
-export const createMetafilePlugin: ReactNativeEsbuildPluginCreator = (
- context,
-) => ({
- name: NAME,
- setup: (build): void => {
- build.onEnd(async (result: BuildResult) => {
- const { metafile } = result;
- const filename = path.join(
- context.root,
- `metafile-${context.platform}-${new Date().getTime().toString()}.json`,
- );
-
- if (metafile) {
- logger.debug('writing esbuild metafile', { destination: filename });
- await fs.writeFile(filename, JSON.stringify(metafile), {
- encoding: 'utf-8',
- });
- }
- });
- },
-});
diff --git a/packages/cli/lib/presets/index.ts b/packages/core/lib/bundler/plugins/presets.ts
similarity index 61%
rename from packages/cli/lib/presets/index.ts
rename to packages/core/lib/bundler/plugins/presets.ts
index 86589491..36b2e230 100644
--- a/packages/cli/lib/presets/index.ts
+++ b/packages/core/lib/bundler/plugins/presets.ts
@@ -1,21 +1,19 @@
-import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
import {
createAssetRegisterPlugin,
createReactNativeRuntimeTransformPlugin,
createReactNativeWebPlugin,
createSvgTransformPlugin,
} from '@react-native-esbuild/plugins';
+import type { PluginFactory } from '@react-native-esbuild/shared';
-const native: ReactNativeEsbuildPluginCreator[] = [
+export const native: PluginFactory[] = [
createAssetRegisterPlugin,
createSvgTransformPlugin,
createReactNativeRuntimeTransformPlugin,
];
-const web: ReactNativeEsbuildPluginCreator[] = [
+export const web: PluginFactory[] = [
createReactNativeWebPlugin,
createSvgTransformPlugin,
createReactNativeRuntimeTransformPlugin,
];
-
-export const presets = { native, web };
diff --git a/packages/core/lib/bundler/plugins/statusPlugin/index.ts b/packages/core/lib/bundler/plugins/statusPlugin/index.ts
deleted file mode 100644
index f8c99b19..00000000
--- a/packages/core/lib/bundler/plugins/statusPlugin/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { createBuildStatusPlugin } from './statusPlugin';
diff --git a/packages/core/lib/bundler/storages/CacheStorage.ts b/packages/core/lib/bundler/storages/CacheStorage.ts
deleted file mode 100644
index c62a94c4..00000000
--- a/packages/core/lib/bundler/storages/CacheStorage.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import os from 'node:os';
-import fs from 'node:fs';
-import path from 'node:path';
-import { GLOBAL_CACHE_DIR } from '@react-native-esbuild/config';
-import { logger } from '../../shared';
-import { CacheController } from '../cache';
-import { Storage } from './Storage';
-
-const CACHE_DIRECTORY = path.join(os.tmpdir(), GLOBAL_CACHE_DIR);
-
-export class CacheStorage extends Storage {
- constructor() {
- super();
- try {
- fs.accessSync(CACHE_DIRECTORY, fs.constants.R_OK | fs.constants.W_OK);
- } catch (_error) {
- logger.debug('cache directory is not exist or no access permission');
- fs.mkdirSync(CACHE_DIRECTORY, { recursive: true });
- }
- }
-
- public get(key: number): CacheController {
- if (this.data.has(key)) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Already `has()` checked.
- return this.data.get(key)!;
- }
-
- const controller = new CacheController(CACHE_DIRECTORY);
- this.data.set(key, controller);
-
- return controller;
- }
-
- public clearAll(): Promise {
- for (const controller of this.data.values()) {
- controller.reset();
- }
- return fs.promises.rm(CACHE_DIRECTORY, {
- recursive: true,
- });
- }
-}
diff --git a/packages/core/lib/bundler/storages/SharedStorage.ts b/packages/core/lib/bundler/storages/SharedStorage.ts
deleted file mode 100644
index 05c2389e..00000000
--- a/packages/core/lib/bundler/storages/SharedStorage.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import type { BundlerSharedData } from '../../types';
-import { Storage } from './Storage';
-
-export class SharedStorage extends Storage {
- private getDefaultSharedData(): BundlerSharedData {
- return {
- watcher: {
- changed: null,
- stats: undefined,
- },
- };
- }
-
- public get(key: number): BundlerSharedData {
- if (this.data.has(key)) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Already `has()` checked.
- return this.data.get(key)!;
- }
-
- const sharedData = this.getDefaultSharedData();
- this.data.set(key, sharedData);
-
- return sharedData;
- }
-
- public setValue(value: Partial): void {
- for (const sharedData of this.data.values()) {
- sharedData.watcher.changed =
- value.watcher?.changed ?? sharedData.watcher.changed;
- sharedData.watcher.stats =
- value.watcher?.stats ?? sharedData.watcher.stats;
- }
- }
-
- public clearAll(): Promise {
- for (const sharedData of this.data.values()) {
- sharedData.watcher.changed = null;
- sharedData.watcher.stats = undefined;
- }
- return Promise.resolve();
- }
-}
diff --git a/packages/core/lib/bundler/storages/Storage.ts b/packages/core/lib/bundler/storages/Storage.ts
deleted file mode 100644
index 83433322..00000000
--- a/packages/core/lib/bundler/storages/Storage.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export abstract class Storage {
- protected data = new Map();
- public abstract get(key: number): StorageData;
- public abstract clearAll(): Promise;
-}
diff --git a/packages/core/lib/bundler/storages/index.ts b/packages/core/lib/bundler/storages/index.ts
deleted file mode 100644
index 5fcca858..00000000
--- a/packages/core/lib/bundler/storages/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './CacheStorage';
-export * from './SharedStorage';
diff --git a/packages/core/lib/watcher/FileSystemWatcher.ts b/packages/core/lib/bundler/watcher/FileSystemWatcher.ts
similarity index 77%
rename from packages/core/lib/watcher/FileSystemWatcher.ts
rename to packages/core/lib/bundler/watcher/FileSystemWatcher.ts
index cf167382..189e42a6 100644
--- a/packages/core/lib/watcher/FileSystemWatcher.ts
+++ b/packages/core/lib/bundler/watcher/FileSystemWatcher.ts
@@ -4,7 +4,8 @@ import {
SOURCE_EXTENSIONS,
ASSET_EXTENSIONS,
} from '@react-native-esbuild/internal';
-import { logger } from '../shared';
+import { logger } from '../../shared';
+import type { FileSystemWatchEventListener } from '../../types';
const WATCH_EXTENSIONS_REGEXP = new RegExp(
`(?:${[...SOURCE_EXTENSIONS, ...ASSET_EXTENSIONS].join('|')})$`,
@@ -12,21 +13,10 @@ const WATCH_EXTENSIONS_REGEXP = new RegExp(
export class FileSystemWatcher {
public static DEBOUNCE_DELAY = 300;
- private static instance: FileSystemWatcher | null = null;
private watcher: chokidar.FSWatcher | null = null;
private debounceTimer?: NodeJS.Timeout;
- private onWatch?: (event: string, path: string, stats?: Stats) => void;
- private constructor() {
- /* Empty constructor */
- }
-
- public static getInstance(): FileSystemWatcher {
- if (FileSystemWatcher.instance === null) {
- FileSystemWatcher.instance = new FileSystemWatcher();
- }
- return FileSystemWatcher.instance;
- }
+ constructor(private listener: FileSystemWatchEventListener) {}
private handleWatch(event: string, path: string, stats?: Stats): void {
if (
@@ -38,18 +28,11 @@ export class FileSystemWatcher {
this.debounceTimer = setTimeout(() => {
this.debounceTimer = undefined;
- this.onWatch?.(event, path, stats);
+ this.listener.onFileSystemChange(event, path, stats);
logger.debug('changes detected by watcher', { event, path });
}, FileSystemWatcher.DEBOUNCE_DELAY);
}
- setHandler(
- handler: (event: string, path: string, stats?: Stats) => void,
- ): this {
- this.onWatch = handler;
- return this;
- }
-
watch(targetPath: string): Promise {
if (this.watcher) {
logger.debug('already watching');
diff --git a/packages/core/lib/watcher/index.ts b/packages/core/lib/bundler/watcher/index.ts
similarity index 100%
rename from packages/core/lib/watcher/index.ts
rename to packages/core/lib/bundler/watcher/index.ts
diff --git a/packages/core/lib/bundler/helpers/__tests__/config.test.ts b/packages/core/lib/helpers/__tests__/config.test.ts
similarity index 95%
rename from packages/core/lib/bundler/helpers/__tests__/config.test.ts
rename to packages/core/lib/helpers/__tests__/config.test.ts
index 22d29158..fc793073 100644
--- a/packages/core/lib/bundler/helpers/__tests__/config.test.ts
+++ b/packages/core/lib/helpers/__tests__/config.test.ts
@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker';
+import type { Config } from '@react-native-esbuild/shared';
import { loadConfig, getConfigFromGlobal } from '../config';
-import type { Config } from '../../../types';
describe('config helpers', () => {
describe('loadConfig', () => {
diff --git a/packages/core/lib/helpers/buildTask.ts b/packages/core/lib/helpers/buildTask.ts
new file mode 100644
index 00000000..2482211c
--- /dev/null
+++ b/packages/core/lib/helpers/buildTask.ts
@@ -0,0 +1,19 @@
+import invariant from 'invariant';
+import type { BundleResult, BuildTaskDelegate } from '../types';
+
+export const createBuildTaskDelegate = (): BuildTaskDelegate => {
+ let resolver: BuildTaskDelegate['success'] | undefined;
+ let rejecter: BuildTaskDelegate['failure'] | undefined;
+
+ const task = new Promise((resolve, _reject) => {
+ resolver = resolve;
+ rejecter = (reason: Error) => {
+ resolve({ result: null, error: reason });
+ };
+ });
+
+ invariant(resolver, 'resolver is undefined');
+ invariant(rejecter, 'rejecter is undefined');
+
+ return { promise: task, success: resolver, failure: rejecter };
+};
diff --git a/packages/core/lib/bundler/helpers/config.ts b/packages/core/lib/helpers/config.ts
similarity index 76%
rename from packages/core/lib/bundler/helpers/config.ts
rename to packages/core/lib/helpers/config.ts
index 8c83fd43..cfc129ee 100644
--- a/packages/core/lib/bundler/helpers/config.ts
+++ b/packages/core/lib/helpers/config.ts
@@ -1,17 +1,18 @@
import path from 'node:path';
import type { BuildOptions } from 'esbuild';
import deepmerge from 'deepmerge';
+import {
+ getDevServerPublicPath,
+ BuildMode,
+ type BundleOptions,
+ type Config,
+} from '@react-native-esbuild/shared';
import {
RESOLVER_MAIN_FIELDS,
SOURCE_EXTENSIONS,
ASSET_EXTENSIONS,
} from '@react-native-esbuild/internal';
-import {
- getDevServerPublicPath,
- type BundleOptions,
-} from '@react-native-esbuild/config';
-import { logger } from '../../shared';
-import type { BundleMode, Config } from '../../types';
+import { logger } from '../shared';
export const loadConfig = (configFilePath?: string): Config => {
let config: Config | undefined;
@@ -29,13 +30,6 @@ export const loadConfig = (configFilePath?: string): Config => {
assetExtensions: ASSET_EXTENSIONS,
},
transformer: {
- jsc: {
- transform: {
- react: {
- runtime: 'automatic',
- },
- },
- },
stripFlowPackageNames: ['react-native'],
},
web: {
@@ -44,12 +38,11 @@ export const loadConfig = (configFilePath?: string): Config => {
};
try {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires -- Config file may not exist.
+ // eslint-disable-next-line @typescript-eslint/no-var-requires -- Config file may not exist.
config = require(
configFilePath
? path.resolve(configFilePath)
: path.resolve(process.cwd(), 'react-native-esbuild.config.js'),
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Config will be default exported.
).default;
} catch (error) {
/**
@@ -85,7 +78,7 @@ export const getConfigFromGlobal = (): Config => {
};
export const getEsbuildWebConfig = (
- mode: BundleMode,
+ mode: BuildMode,
root: string,
bundleOptions: BundleOptions,
): BuildOptions => {
@@ -97,7 +90,8 @@ export const getEsbuildWebConfig = (
* - bundle mode: `outfile`
* - watch mode:`outdir`
*/
- outdir: mode === 'bundle' ? undefined : getDevServerPublicPath(root),
- outfile: mode === 'bundle' ? bundleOptions.outfile : undefined,
+ outdir:
+ mode === BuildMode.Bundle ? undefined : getDevServerPublicPath(root),
+ outfile: mode === BuildMode.Bundle ? bundleOptions.outfile : undefined,
};
};
diff --git a/packages/core/lib/helpers/fs.ts b/packages/core/lib/helpers/fs.ts
new file mode 100644
index 00000000..2223cce5
--- /dev/null
+++ b/packages/core/lib/helpers/fs.ts
@@ -0,0 +1,13 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+export const getExternalFromPackageJson = (root: string): string[] => {
+ const { dependencies = {} } = JSON.parse(
+ fs.readFileSync(path.join(root, 'package.json'), 'utf-8'),
+ );
+ return [
+ 'react/jsx-runtime',
+ '@react-navigation/devtools',
+ ...Object.keys(dependencies as Record),
+ ];
+};
diff --git a/packages/core/lib/bundler/helpers/index.ts b/packages/core/lib/helpers/index.ts
similarity index 51%
rename from packages/core/lib/bundler/helpers/index.ts
rename to packages/core/lib/helpers/index.ts
index dd49165c..763ef7dc 100644
--- a/packages/core/lib/bundler/helpers/index.ts
+++ b/packages/core/lib/helpers/index.ts
@@ -1,3 +1,4 @@
-export * from './async';
+export * from './buildTask';
export * from './config';
+export * from './fs';
export * from './internal';
diff --git a/packages/core/lib/bundler/helpers/internal.ts b/packages/core/lib/helpers/internal.ts
similarity index 53%
rename from packages/core/lib/bundler/helpers/internal.ts
rename to packages/core/lib/helpers/internal.ts
index 5955e7f3..8c67502b 100644
--- a/packages/core/lib/bundler/helpers/internal.ts
+++ b/packages/core/lib/helpers/internal.ts
@@ -1,38 +1,53 @@
+import fs from 'node:fs/promises';
import type { BuildOptions } from 'esbuild';
-import { getPreludeScript } from '@react-native-esbuild/internal';
-import type { TransformerContext } from '@react-native-esbuild/transformer';
+import type { TransformContext } from '@react-native-esbuild/transformer';
import {
stripFlowWithSucrase,
- minifyWithSwc,
+ transformWithSwc,
swcPresets,
} from '@react-native-esbuild/transformer';
-import type { BundleOptions } from '@react-native-esbuild/config';
+import type { BundleOptions } from '@react-native-esbuild/shared';
+import { getPreludeScript } from '@react-native-esbuild/internal';
+
+const loadScript = (path: string): Promise =>
+ fs.readFile(require.resolve(path), 'utf-8');
export const getTransformedPreludeScript = async (
bundleOptions: BundleOptions,
root: string,
+ additionalScriptPaths?: string[],
): Promise => {
// Dummy context
- const context: TransformerContext = {
+ const context: TransformContext = {
root,
- path: '',
+ path: 'react-native:prelude',
id: 0,
dev: bundleOptions.dev,
entry: bundleOptions.entry,
+ pluginData: {},
};
- const preludeScript = await getPreludeScript(bundleOptions, root);
+
+ const additionalPreludeScripts = await Promise.all(
+ (additionalScriptPaths ?? []).map(loadScript),
+ );
+
+ const preludeScript = [
+ await getPreludeScript(bundleOptions.dev, root),
+ ...additionalPreludeScripts,
+ ].join('\n');
/**
* Remove `"use strict";` added by sucrase.
* @see {@link https://github.com/alangpierce/sucrase/issues/787#issuecomment-1483934492}
*/
- const strippedScript = stripFlowWithSucrase(preludeScript, context)
+ const strippedScript = stripFlowWithSucrase(preludeScript, { context })
.replace(/"use strict";/, '')
.trim();
- return bundleOptions.minify
- ? minifyWithSwc(strippedScript, context, swcPresets.getMinifyPreset())
- : strippedScript;
+ return transformWithSwc(strippedScript, {
+ context,
+ preset: swcPresets.getMinifyPreset({ minify: bundleOptions.minify }),
+ });
};
export const getResolveExtensionsOption = (
@@ -70,3 +85,24 @@ export const getLoaderOption = (
assetExtensions.map((ext) => [ext, 'file'] as const),
);
};
+
+export const getExternalModulePattern = (
+ externalPackages: string[],
+ assetExtensions: string[],
+): string => {
+ const externalPackagePatterns = externalPackages
+ .map((packageName) => `^${packageName}/?$`)
+ .join('|');
+
+ const assetPatterns = [
+ ...assetExtensions,
+ // `.svg` assets will be handled by `svg-transform-plugin`.
+ '.svg',
+ // `.json` contents will be handled by `react-native-runtime-transform-plugin`.
+ '.json',
+ ]
+ .map((extension) => `${extension}$`)
+ .join('|');
+
+ return `(${externalPackagePatterns}|${assetPatterns})`;
+};
diff --git a/packages/core/lib/index.ts b/packages/core/lib/index.ts
index e932ee9b..1699602c 100644
--- a/packages/core/lib/index.ts
+++ b/packages/core/lib/index.ts
@@ -4,6 +4,6 @@ import pkg from '../package.json';
self._version = pkg.version;
export { ReactNativeEsbuildBundler } from './bundler';
+export type { Config } from '@react-native-esbuild/shared';
+export type * from './bundler/events';
export type * from './types';
-export type * from './bundler';
-export type { CacheController } from './bundler/cache';
diff --git a/packages/core/lib/shared.ts b/packages/core/lib/shared.ts
index 5690d4fd..d29f0798 100644
--- a/packages/core/lib/shared.ts
+++ b/packages/core/lib/shared.ts
@@ -1,3 +1,3 @@
-import { Logger } from '@react-native-esbuild/utils';
+import { Logger } from '@react-native-esbuild/shared';
export const logger = new Logger('core');
diff --git a/packages/core/lib/types.ts b/packages/core/lib/types.ts
index 139eb200..72f2ce62 100644
--- a/packages/core/lib/types.ts
+++ b/packages/core/lib/types.ts
@@ -1,147 +1,22 @@
import type { Stats } from 'node:fs';
-import type { BuildContext, Plugin } from 'esbuild';
-import type {
- BabelTransformRule,
- SwcTransformRule,
-} from '@react-native-esbuild/transformer';
-import type { BundleOptions } from '@react-native-esbuild/config';
-import type { JscConfig } from '@swc/core';
-
-export interface Config {
- /**
- * Enable cache.
- *
- * Defaults to `true`
- */
- cache?: boolean;
- /**
- * Logger configurations
- */
- logger?: {
- /**
- * Disable log.
- *
- * Defaults to `false`
- */
- disabled?: boolean;
- /**
- * Print timestamp with log when format is specified.
- *
- * Defaults to `null`
- */
- timestamp?: string | null;
- };
- /**
- * Resolver configurations
- */
- resolver?: {
- /**
- * Field names for resolve package's modules.
- *
- * Defaults to `['react-native', 'browser', 'main', 'module']`
- */
- mainFields?: string[];
- /**
- * File extensions for transform.
- *
- * Defaults: https://github.com/leegeunhyeok/react-native-esbuild/blob/master/packages/internal/lib/defaults.ts
- */
- sourceExtensions?: string[];
- /**
- * File extensions for assets registration.
- *
- * Defaults: https://github.com/leegeunhyeok/react-native-esbuild/blob/master/packages/internal/lib/defaults.ts
- */
- assetExtensions?: string[];
- };
- /**
- * Transformer configurations
- */
- transformer?: {
- /**
- * Swc's `jsc` config.
- */
- jsc: Pick;
- /**
- * Strip flow syntax.
- *
- * Defaults to `['react-native']`
- */
- stripFlowPackageNames?: string[];
- /**
- * Transform with babel using `metro-react-native-babel-preset` (slow)
- */
- fullyTransformPackageNames?: string[];
- /**
- * Additional transform rules. This rules will be applied before phase of transform to es5.
- */
- additionalTransformRules?: {
- /**
- * Additional babel transform rules
- */
- babel?: BabelTransformRule[];
- /**
- * Additional swc transform rules
- */
- swc?: SwcTransformRule[];
- };
- };
- /**
- * Web configurations
- */
- web?: {
- /**
- * Index page template file path
- */
- template?: string;
- /**
- * Placeholders for replacement
- *
- * ```js
- * // web.placeholders
- * { placeholder_name: 'Hello, world!' };
- * ```
- *
- * will be replaced to
- *
- * ```html
- *
- * {{placeholder_name}}
- *
- *
- * Hello, world!
- * ```
- *
- * ---
- *
- * Reserved placeholder name
- *
- * - `root`: root tag id
- * - `script`: bundled script path
- */
- placeholders?: Record;
- };
- /**
- * Additional Esbuild plugins.
- */
- plugins?: Plugin[];
- /**
- * Client event receiver (only work on native)
- */
- reporter?: (event: ReportableEvent) => void;
-}
+import type { BuildContext as EsbuildBuildContext } from 'esbuild';
+import type { BundleOptions } from '@react-native-esbuild/shared';
export interface BundlerInitializeOptions {
watcherEnabled?: boolean;
}
-export type BundleMode = 'bundle' | 'watch';
-
export interface BuildTask {
- context: BuildContext;
- handler: PromiseHandler | null;
- status: 'pending' | 'resolved';
+ esbuild: EsbuildBuildContext;
+ delegate: BuildTaskDelegate;
buildCount: number;
+ status: 'pending' | 'resolved';
+}
+
+export interface BuildTaskDelegate {
+ promise: Promise;
+ success: (val: BundleResult) => void;
+ failure: (reason?: unknown) => void;
}
export type BundleResult = BundleSuccessResult | BundleFailureResult;
@@ -161,28 +36,6 @@ export interface BundleFailureResult {
error: Error;
}
-export type ReactNativeEsbuildPluginCreator = (
- context: PluginContext,
- config?: PluginConfig,
-) => Plugin;
-
-export interface BundlerSharedData {
- watcher: {
- changed: string | null;
- stats?: Stats;
- };
-}
-
-export type BundlerAdditionalData = Record;
-
-export interface PluginContext extends BundleOptions {
- id: number;
- root: string;
- config: Config;
- mode: BundleMode;
- additionalData?: BundlerAdditionalData;
-}
-
export interface BundleRequestOptions {
entry?: BundleOptions['entry'];
dev?: BundleOptions['dev'];
@@ -191,49 +44,11 @@ export interface BundleRequestOptions {
runModule: boolean;
}
-export interface PromiseHandler {
- task: Promise;
- resolver?: (val: Result) => void;
- rejecter?: (reason?: unknown) => void;
-}
-
-export interface Cache {
- data: string;
- modifiedAt: number;
-}
-
-export type ReportableEvent = ClientLogEvent;
-
-/**
- * Event reportable event types
- *
- * @see {@link https://github.com/facebook/metro/blob/v0.78.0/packages/metro/src/lib/reporting.js#L36}
- */
-export interface ClientLogEvent {
- type: 'client_log';
- level:
- | 'trace'
- | 'info'
- | 'warn'
- /**
- * In react-native, ReportableEvent['level'] does not defined `error` type.
- * But, flipper supports the `error` type.
- *
- * @see {@link https://github.com/facebook/flipper/blob/v0.211.0/desktop/flipper-common/src/server-types.tsx#L76}
- */
- | 'error'
- | 'log'
- | 'group'
- | 'groupCollapsed'
- | 'groupEnd'
- | 'debug';
- data: unknown[];
- mode: 'BRIDGE' | 'NOBRIDGE';
-}
-
-// StatusLogger
-export interface BuildStatus {
- total: number;
- resolved: number;
- loaded: number;
+// Watcher
+export interface FileSystemWatchEventListener {
+ onFileSystemChange: (
+ event: string,
+ path: string,
+ stats: Stats | undefined,
+ ) => void;
}
diff --git a/packages/core/package.json b/packages/core/package.json
index 7005cc38..3768e292 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -40,13 +40,15 @@
"node": ">=16"
},
"dependencies": {
- "@react-native-esbuild/config": "workspace:*",
+ "@react-native-esbuild/hmr": "workspace:*",
"@react-native-esbuild/internal": "workspace:*",
+ "@react-native-esbuild/plugins": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"@react-native-esbuild/transformer": "workspace:*",
- "@react-native-esbuild/utils": "workspace:*",
"chokidar": "^3.5.3",
"deepmerge": "^4.3.1",
"esbuild": "^0.19.5",
+ "esbuild-dependency-graph": "^0.3.0",
"invariant": "^2.2.4",
"ora": "^5.4.1"
},
diff --git a/packages/dev-server/lib/helpers/request.ts b/packages/dev-server/lib/helpers/request.ts
index fe749302..44cd90c5 100644
--- a/packages/dev-server/lib/helpers/request.ts
+++ b/packages/dev-server/lib/helpers/request.ts
@@ -1,6 +1,6 @@
import { parse } from 'node:url';
import { z } from 'zod';
-import type { BundleOptions } from '@react-native-esbuild/config';
+import type { BundleOptions } from '@react-native-esbuild/shared';
import { BundleRequestType } from '../types';
export type ParsedBundleOptions = z.infer;
diff --git a/packages/dev-server/lib/middlewares/__tests__/serveAsset.test.ts b/packages/dev-server/lib/middlewares/__tests__/serveAsset.test.ts
index 0f43de5d..f0a2fc77 100644
--- a/packages/dev-server/lib/middlewares/__tests__/serveAsset.test.ts
+++ b/packages/dev-server/lib/middlewares/__tests__/serveAsset.test.ts
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
import path from 'node:path';
import fs from 'node:fs/promises';
import type { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
-import { ASSET_PATH } from '@react-native-esbuild/config';
+import { ASSET_PATH } from '@react-native-esbuild/shared';
import { faker } from '@faker-js/faker';
import { createServeAssetMiddleware } from '../serveAsset';
import type { DevServerMiddleware } from '../../types';
diff --git a/packages/dev-server/lib/middlewares/hmr.ts b/packages/dev-server/lib/middlewares/hmr.ts
new file mode 100644
index 00000000..8dd2a41c
--- /dev/null
+++ b/packages/dev-server/lib/middlewares/hmr.ts
@@ -0,0 +1,111 @@
+import {
+ HmrAppServer,
+ HmrWebServer,
+ type HMRClientMessage,
+} from '@react-native-esbuild/hmr';
+import { getReloadByDevSettingsProxy } from '@react-native-esbuild/internal';
+import type { HMRMiddleware } from '../types';
+import { logger } from '../shared';
+
+export const createHMRMiddlewareForApp = ({
+ onMessage,
+}: {
+ onMessage?: (event: HMRClientMessage) => void;
+}): HMRMiddleware => {
+ const server = new HmrAppServer();
+
+ server.setMessageHandler((event) => onMessage?.(event));
+
+ const updateStart = (): void => {
+ logger.debug('send update-start message');
+ server.send('update-start', { isInitialUpdate: false });
+ };
+
+ const updateDone = (): void => {
+ logger.debug('send update-done message');
+ server.send('update-done', undefined);
+ };
+
+ const hotReload = (revisionId: string, code: string): void => {
+ logger.debug('send update message (hmr)');
+ server.send('update', {
+ added: [
+ {
+ module: [-1, code],
+ sourceMappingURL: null,
+ sourceURL: null,
+ },
+ ],
+ deleted: [],
+ modified: [],
+ isInitialUpdate: false,
+ revisionId,
+ });
+ };
+
+ const liveReload = (revisionId: string): void => {
+ logger.debug('send update message (live reload)');
+ server.send('update', {
+ added: [
+ {
+ module: [-1, getReloadByDevSettingsProxy()],
+ sourceMappingURL: null,
+ sourceURL: null,
+ },
+ ],
+ deleted: [],
+ modified: [],
+ isInitialUpdate: false,
+ revisionId,
+ });
+ };
+
+ return { server, updateStart, updateDone, hotReload, liveReload };
+};
+
+export const createHMRMiddlewareForWeb = (): HMRMiddleware => {
+ const server = new HmrWebServer();
+
+ // eslint-disable-next-line @typescript-eslint/no-empty-function -- noop
+ const noop = (): void => {};
+
+ return {
+ server,
+ updateStart: noop,
+ updateDone: noop,
+ hotReload: (_revisionId: string, _code: string): void => {
+ logger.debug('send update message (hmr)');
+ // TODO
+ // server.send('update', {
+ // added: [
+ // {
+ // module: [-1, code],
+ // sourceMappingURL: null,
+ // sourceURL: null,
+ // },
+ // ],
+ // deleted: [],
+ // modified: [],
+ // isInitialUpdate: false,
+ // revisionId,
+ // });
+ },
+ liveReload: (_revisionId: string): void => {
+ logger.debug('send update message (live reload)');
+ // TODO
+ // server.send('update', {
+ // added: [
+ // {
+ // module: [-1, 'window.location.reload();'],
+ // sourceMappingURL: null,
+ // sourceURL: null,
+ // },
+ // ],
+ // deleted: [],
+ // modified: [],
+ // isInitialUpdate: false,
+ // revisionId,
+ // });
+ },
+ };
+};
diff --git a/packages/dev-server/lib/middlewares/hotReload.ts b/packages/dev-server/lib/middlewares/hotReload.ts
deleted file mode 100644
index 3ce6a213..00000000
--- a/packages/dev-server/lib/middlewares/hotReload.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import { Server, type WebSocket, type MessageEvent, type Data } from 'ws';
-import type { ClientLogEvent } from '@react-native-esbuild/core';
-import { getReloadByDevSettingsProxy } from '@react-native-esbuild/internal';
-import { logger } from '../shared';
-import type {
- HotReloadMiddleware,
- HmrClientMessage,
- HmrUpdateDoneMessage,
- HmrUpdateMessage,
- HmrUpdateStartMessage,
-} from '../types';
-
-const getMessage = (data: Data): HmrClientMessage | null => {
- try {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Web socket data.
- const parsedData = JSON.parse(String(data));
- return 'type' in parsedData ? (parsedData as HmrClientMessage) : null;
- } catch (error) {
- return null;
- }
-};
-
-export const createHotReloadMiddleware = ({
- onLog,
-}: {
- onLog?: (event: ClientLogEvent) => void;
-}): HotReloadMiddleware => {
- const server = new Server({ noServer: true });
- let connectedSocket: WebSocket | null = null;
-
- const handleClose = (): void => {
- connectedSocket = null;
- logger.debug('HMR web socket was closed');
- };
-
- const handleMessage = (event: MessageEvent): void => {
- const message = getMessage(event.data);
- if (!message) return;
-
- /**
- * @see {@link https://github.com/facebook/metro/blob/v0.77.0/packages/metro/src/HmrServer.js#L200-L239}
- */
- switch (message.type) {
- case 'log': {
- onLog?.({
- type: 'client_log',
- level: message.level,
- data: message.data,
- mode: 'BRIDGE',
- });
- break;
- }
-
- // Not supported
- case 'register-entrypoints':
- case 'log-opt-in':
- break;
- }
- };
-
- const handleError = (error?: Error): void => {
- if (error) {
- logger.error('unable to send HMR update message', error);
- }
- };
-
- /**
- * Send reload code to client.
- *
- * @see {@link https://github.com/facebook/metro/blob/v0.77.0/packages/metro-runtime/src/modules/HMRClient.js#L91-L99}
- */
- const hotReload = (revisionId: string): void => {
- const hmrUpdateMessage: HmrUpdateMessage = {
- type: 'update',
- body: {
- added: [
- {
- module: [-1, getReloadByDevSettingsProxy()],
- sourceMappingURL: null,
- sourceURL: null,
- },
- ],
- deleted: [],
- modified: [],
- isInitialUpdate: false,
- revisionId,
- },
- };
-
- logger.debug('sending update message with reload code');
- connectedSocket?.send(JSON.stringify(hmrUpdateMessage), handleError);
- };
-
- const updateStart = (): void => {
- logger.debug('sending update-start');
- const hmrUpdateStartMessage: HmrUpdateStartMessage = {
- type: 'update-start',
- body: {
- isInitialUpdate: false,
- },
- };
- connectedSocket?.send(JSON.stringify(hmrUpdateStartMessage), handleError);
- };
-
- const updateDone = (): void => {
- logger.debug('sending update-done');
- const hmrUpdateDoneMessage: HmrUpdateDoneMessage = { type: 'update-done' };
- connectedSocket?.send(JSON.stringify(hmrUpdateDoneMessage), handleError);
- };
-
- server.on('connection', (socket) => {
- connectedSocket = socket;
- connectedSocket.onerror = handleClose;
- connectedSocket.onclose = handleClose;
- connectedSocket.onmessage = handleMessage;
- logger.debug('HMR web socket was connected');
- });
-
- server.on('error', (error) => {
- logger.error('HMR web socket server error', error);
- });
-
- return { server, hotReload, updateStart, updateDone };
-};
diff --git a/packages/dev-server/lib/middlewares/index.ts b/packages/dev-server/lib/middlewares/index.ts
index 6e88227f..caf48ffb 100644
--- a/packages/dev-server/lib/middlewares/index.ts
+++ b/packages/dev-server/lib/middlewares/index.ts
@@ -1,4 +1,4 @@
-export * from './hotReload';
+export * from './hmr';
export * from './indexPage';
export * from './serveAsset';
export * from './serveBundle';
diff --git a/packages/dev-server/lib/middlewares/serveAsset.ts b/packages/dev-server/lib/middlewares/serveAsset.ts
index cbb32143..0d1271f1 100644
--- a/packages/dev-server/lib/middlewares/serveAsset.ts
+++ b/packages/dev-server/lib/middlewares/serveAsset.ts
@@ -2,7 +2,7 @@ import fs, { type FileHandle } from 'node:fs/promises';
import path from 'node:path';
import url from 'node:url';
import mime from 'mime';
-import { ASSET_PATH } from '@react-native-esbuild/config';
+import { ASSET_PATH } from '@react-native-esbuild/shared';
import { logger } from '../shared';
import type { DevServerMiddlewareCreator } from '../types';
diff --git a/packages/dev-server/lib/middlewares/serveBundle.ts b/packages/dev-server/lib/middlewares/serveBundle.ts
index 84c85939..99aa8ce4 100644
--- a/packages/dev-server/lib/middlewares/serveBundle.ts
+++ b/packages/dev-server/lib/middlewares/serveBundle.ts
@@ -3,7 +3,7 @@ import type {
BundlerEventListener,
ReactNativeEsbuildBundler,
} from '@react-native-esbuild/core';
-import { getIdByOptions } from '@react-native-esbuild/config';
+import { getIdByOptions } from '@react-native-esbuild/shared';
import {
BundleResponse,
parseBundleOptionsFromRequestUrl,
diff --git a/packages/dev-server/lib/middlewares/symbolicate.ts b/packages/dev-server/lib/middlewares/symbolicate.ts
index be726501..f9601862 100644
--- a/packages/dev-server/lib/middlewares/symbolicate.ts
+++ b/packages/dev-server/lib/middlewares/symbolicate.ts
@@ -3,7 +3,7 @@ import {
parseStackFromRawBody,
symbolicateStackTrace,
} from '@react-native-esbuild/symbolicate';
-import type { BundleOptions } from '@react-native-esbuild/config';
+import type { BundleOptions } from '@react-native-esbuild/shared';
import {
parseBundleOptionsForWeb,
parseBundleOptionsFromRequestUrl,
diff --git a/packages/dev-server/lib/server/ReactNativeAppServer.ts b/packages/dev-server/lib/server/ReactNativeAppServer.ts
index 1a18b19c..849ee6ae 100644
--- a/packages/dev-server/lib/server/ReactNativeAppServer.ts
+++ b/packages/dev-server/lib/server/ReactNativeAppServer.ts
@@ -7,7 +7,7 @@ import { InspectorProxy } from 'metro-inspector-proxy';
import { createDevServerMiddleware } from '@react-native-community/cli-server-api';
import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
import {
- createHotReloadMiddleware,
+ createHMRMiddlewareForApp,
createServeAssetMiddleware,
createServeBundleMiddleware,
createSymbolicateMiddleware,
@@ -105,10 +105,26 @@ export class ReactNativeAppServer extends DevServer {
throw new Error('server is not initialized');
}
- const { server: hotReloadWss, ...hr } = createHotReloadMiddleware({
- onLog: (event) => {
- this.eventsSocketEndpoint.reportEvent(event);
- this.bundler?.emit('report', event);
+ const { server: hmrServer, ...hmr } = createHMRMiddlewareForApp({
+ onMessage: (message) => {
+ switch (message.type) {
+ case 'log': {
+ const clientLogEvent = {
+ type: 'client_log',
+ level: message.level,
+ data: message.data,
+ mode: 'BRIDGE',
+ } as const;
+ this.bundler?.emit('report', clientLogEvent);
+ this.eventsSocketEndpoint.reportEvent(clientLogEvent);
+ break;
+ }
+
+ // not supported
+ case 'register-entrypoints':
+ case 'log-opt-in':
+ break;
+ }
},
});
@@ -123,20 +139,22 @@ export class ReactNativeAppServer extends DevServer {
);
const webSocketServer: Record = {
- '/hot': hotReloadWss,
+ '/hot': hmrServer.getWebSocketServer(),
'/debugger-proxy': this.debuggerProxyEndpoint.server,
'/message': this.messageSocketEndpoint.server,
'/events': this.eventsSocketEndpoint.server,
...inspectorProxyWss,
};
- this.bundler.on('build-start', hr.updateStart);
- this.bundler.on('build-end', ({ revisionId, additionalData }) => {
+ this.bundler.on('build-start', hmr.updateStart);
+ this.bundler.on('build-end', ({ revisionId, update, additionalData }) => {
// `additionalData` can be `{ disableRefresh: true }` by `serve-asset-middleware`.
if (!additionalData?.disableRefresh) {
- hr.hotReload(revisionId);
+ update === null || update.fullyReload
+ ? hmr.liveReload(revisionId)
+ : hmr.hotReload(revisionId, update.code);
}
- hr.updateDone();
+ hmr.updateDone();
});
this.server.on('upgrade', (request, socket, head) => {
diff --git a/packages/dev-server/lib/server/ReactNativeWebServer.ts b/packages/dev-server/lib/server/ReactNativeWebServer.ts
index c9fd5f1e..b7201de3 100644
--- a/packages/dev-server/lib/server/ReactNativeWebServer.ts
+++ b/packages/dev-server/lib/server/ReactNativeWebServer.ts
@@ -3,14 +3,18 @@ import http, {
type ServerResponse,
type IncomingMessage,
} from 'node:http';
+import { parse } from 'node:url';
import type { ServeResult } from 'esbuild';
import invariant from 'invariant';
import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
import {
combineWithDefaultBundleOptions,
type BundleOptions,
-} from '@react-native-esbuild/config';
-import { createSymbolicateMiddleware } from '../middlewares';
+} from '@react-native-esbuild/shared';
+import {
+ createHMRMiddlewareForWeb,
+ createSymbolicateMiddleware,
+} from '../middlewares';
import { logger } from '../shared';
import type { DevServerOptions } from '../types';
import { DevServer } from './DevServer';
@@ -82,9 +86,7 @@ export class ReactNativeWebServer extends DevServer {
});
}
- async initialize(
- onPostSetup?: (bundler: ReactNativeEsbuildBundler) => void | Promise,
- ): Promise {
+ async initialize(): Promise {
if (this.initialized) {
logger.warn('dev server already initialized');
return this;
@@ -96,6 +98,16 @@ export class ReactNativeWebServer extends DevServer {
this.devServerOptions.root,
).initialize({ watcherEnabled: true }));
+ const { server: hmrServer, ...hmr } = createHMRMiddlewareForWeb();
+
+ this.bundler.on('build-end', ({ revisionId, update, additionalData }) => {
+ if (!additionalData?.disableRefresh) {
+ update?.fullyReload
+ ? hmr.liveReload(revisionId)
+ : hmr.hotReload(revisionId, update?.code ?? '');
+ }
+ });
+
const symbolicateMiddleware = createSymbolicateMiddleware(
{ bundler, devServerOptions: this.devServerOptions },
{ webBundleOptions: this.bundleOptions },
@@ -114,7 +126,19 @@ export class ReactNativeWebServer extends DevServer {
});
});
- await onPostSetup?.(bundler);
+ this.server.on('upgrade', (request, socket, head) => {
+ if (!request.url) return;
+ const { pathname } = parse(request.url);
+
+ if (pathname === '/hot') {
+ const wss = hmrServer.getWebSocketServer();
+ wss.handleUpgrade(request, socket, head, (client) => {
+ wss.emit('connection', client, request);
+ });
+ } else {
+ socket.destroy();
+ }
+ });
return this;
}
diff --git a/packages/dev-server/lib/shared.ts b/packages/dev-server/lib/shared.ts
index a8830816..6d5573c6 100644
--- a/packages/dev-server/lib/shared.ts
+++ b/packages/dev-server/lib/shared.ts
@@ -1,3 +1,3 @@
-import { Logger } from '@react-native-esbuild/utils';
+import { Logger } from '@react-native-esbuild/shared';
export const logger = new Logger('dev-server');
diff --git a/packages/dev-server/lib/types.ts b/packages/dev-server/lib/types.ts
index 36ac5c11..20c425d3 100644
--- a/packages/dev-server/lib/types.ts
+++ b/packages/dev-server/lib/types.ts
@@ -1,6 +1,6 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
-import type { Server as WebSocketServer } from 'ws';
import type { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
+import type { HMRServer } from '@react-native-esbuild/hmr';
export enum BundleRequestType {
Unknown,
@@ -30,22 +30,14 @@ export type DevServerMiddleware = (
next: (error?: unknown) => void,
) => void;
-export interface HotReloadMiddleware {
- server: WebSocketServer;
- hotReload: (revisionId: string) => void;
+export interface HMRMiddleware {
+ server: HMRServer;
updateStart: () => void;
updateDone: () => void;
+ hotReload: (revisionId: string, code: string) => void;
+ liveReload: (revisionId: string) => void;
}
-/**
- * HMR web socket messages
- * @see {@link https://github.com/facebook/metro/blob/v0.77.0/packages/metro-runtime/src/modules/types.flow.js#L68}
- */
-export type HmrClientMessage =
- | RegisterEntryPointsMessage
- | LogMessage
- | LogOptInMessage;
-
export interface RegisterEntryPointsMessage {
type: 'register-entrypoints';
entryPoints: string[];
diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json
index ed84c8a9..60788afc 100644
--- a/packages/dev-server/package.json
+++ b/packages/dev-server/package.json
@@ -47,11 +47,11 @@
},
"dependencies": {
"@react-native-community/cli-server-api": "^11.3.6",
- "@react-native-esbuild/config": "workspace:*",
"@react-native-esbuild/core": "workspace:*",
+ "@react-native-esbuild/hmr": "workspace:*",
"@react-native-esbuild/internal": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"@react-native-esbuild/symbolicate": "workspace:*",
- "@react-native-esbuild/utils": "workspace:*",
"invariant": "^2.2.4",
"metro-inspector-proxy": "^0.78.0",
"mime": "^3.0.0",
diff --git a/packages/hmr/README.md b/packages/hmr/README.md
new file mode 100644
index 00000000..3c587cde
--- /dev/null
+++ b/packages/hmr/README.md
@@ -0,0 +1,52 @@
+# `@react-native-esbuild/hmr`
+
+> `react-refresh` based HMR implementation for @react-native-esbuild
+
+## Usage
+
+1. Add import statement to top of entry file(`import 'hmr:runtime';`)
+2. Wrap React component module with `wrapWithHmrBoundary`
+
+```js
+import {
+ getHmrRuntimeInitializeScript,
+ wrapWithHmrBoundary,
+ HMR_RUNTIME_IMPORT_NAME
+} from '@react-native-esbuild/hmr';
+
+// In esbuild plugin
+build.onResolve({ filter: new RegExp(HMR_RUNTIME_IMPORT_NAME) }, (args) => {
+ return {
+ path: args.path,
+ namespace: 'hmr-runtime',
+ };
+});
+
+build.onLoad({ filter: /(?:.*)/, namespace: 'hmr-runtime' }, (args) => {
+ return {
+ js: await getHmrRuntimeInitializeScript(),
+ loader: 'js',
+ };
+});
+
+build.onLoad({ filter: /* some filter */ }, (args) => {
+ if (isEntryFile) {
+ code = await fs.readFile(args.path, 'utf-8');
+ code = `import ${HMR_RUNTIME_IMPORT_NAME};\n\n` + code;
+ // ...
+
+ return {
+ contents: code,
+ loader: 'js',
+ };
+ } else {
+ // ...
+
+ return {
+ // code, component name, module id
+ contents: wrapWithHmrBoundary(code, '', args.path),
+ loader: 'js',
+ };
+ }
+});
+```
diff --git a/packages/hmr/build/index.js b/packages/hmr/build/index.js
new file mode 100644
index 00000000..9ea82862
--- /dev/null
+++ b/packages/hmr/build/index.js
@@ -0,0 +1,25 @@
+const path = require('node:path');
+const esbuild = require('esbuild');
+const { getEsbuildBaseOptions, getPackageRoot } = require('../../../shared');
+const { name, version } = require('../package.json');
+
+const buildOptions = getEsbuildBaseOptions(__dirname);
+const root = getPackageRoot(__dirname);
+
+(async () => {
+ // package
+ await esbuild.build(buildOptions);
+
+ // runtime
+ await esbuild.build({
+ entryPoints: [path.join(root, './lib/runtime/setup.ts')],
+ outfile: 'dist/runtime.js',
+ bundle: true,
+ banner: {
+ js: `// ${name}@${version} runtime`,
+ },
+ });
+})().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/packages/hmr/lib/constants.ts b/packages/hmr/lib/constants.ts
new file mode 100644
index 00000000..f2806bec
--- /dev/null
+++ b/packages/hmr/lib/constants.ts
@@ -0,0 +1,11 @@
+/**
+ * WARNING: Property and function identifiers must match the names defined in `types.ts`.
+ */
+export const HMR_REGISTER_FUNCTION = 'global.__hmr.register';
+export const HMR_UPDATE_FUNCTION = 'global.__hmr.update';
+export const REACT_REFRESH_REGISTER_FUNCTION =
+ 'global.__hmr.reactRefresh.register';
+export const REACT_REFRESH_GET_SIGNATURE_FUNCTION =
+ 'global.__hmr.reactRefresh.getSignatureFunction';
+export const PERFORM_REACT_REFRESH_SCRIPT =
+ 'global.__hmr.reactRefresh.performReactRefresh()';
diff --git a/packages/hmr/lib/helpers.ts b/packages/hmr/lib/helpers.ts
new file mode 100644
index 00000000..ff14193d
--- /dev/null
+++ b/packages/hmr/lib/helpers.ts
@@ -0,0 +1,129 @@
+import path from 'node:path';
+import {
+ colors,
+ type BuildContext,
+ type ModuleId,
+} from '@react-native-esbuild/shared';
+import type { Metafile } from 'esbuild';
+import { getReloadByDevSettingsProxy } from '@react-native-esbuild/internal';
+import { HMR_REGISTER_FUNCTION, HMR_UPDATE_FUNCTION } from './constants';
+import { logger } from './shared';
+
+let INTERNAL_boundaryIndex = 0;
+
+export const asHMRBoundary = (id: ModuleId, body: string): string => {
+ const ident = `__hmr${INTERNAL_boundaryIndex++}`;
+ return `var ${ident} = ${HMR_REGISTER_FUNCTION}(${JSON.stringify(id)});
+ ${body}
+ ${ident}.dispose(function () { });
+ ${ident}.accept(function (payload) {
+ global.__hmr.reactRefresh.performReactRefresh();
+ });`;
+};
+
+/**
+ * Make code that wrap body with HMR API's update callback.
+ *
+ * **example**
+ * ```js
+ * global.__hmr.update(id, function () {
+ * {body}
+ * });
+ * ```
+ */
+export const asHMRUpdateCall = (id: ModuleId, body: string): string => {
+ return `${HMR_UPDATE_FUNCTION}(${JSON.stringify(id)}, function () {
+ ${body}
+ });`;
+};
+
+/**
+ * Make code that wrap with try-catch block with fallback handler
+ *
+ * **example**
+ * ```js
+ * try {
+ * {body}
+ * } catch (error) {
+ * console.error('[HMR] unable to accept', error);
+ * reload();
+ * }
+ * ```
+ */
+export const asFallbackBoundary = (body: string): string => {
+ return `try {
+ ${body}
+ } catch (error) {
+ console.error('[HMR] unable to accept', error);
+ ${getReloadByDevSettingsProxy()}
+ }`;
+};
+
+/**
+ * Make code that register as external module.
+ *
+ * **example**
+ * ```js
+ * {body}
+ * global.__modules.external(id, identName);
+ * ```
+ */
+export const registerAsExternalModule = (
+ id: ModuleId,
+ body: string,
+ identifierName: string,
+): string => {
+ return `${body}\nglobal.__modules.external(${JSON.stringify(
+ id,
+ )}, ${identifierName});`;
+};
+
+export const isHMRBoundary = (path: string): boolean => {
+ return !path.includes('/node_modules/') && !path.endsWith('runtime.js');
+};
+
+/**
+ * Get actual imported module path.
+ */
+export const getActualImportPaths = (
+ buildContext: BuildContext,
+ imports: Metafile['inputs'][string]['imports'],
+): Record => {
+ const importPaths = imports.reduce(
+ (prev, curr) => {
+ // To avoid wrong assets path.
+ // eg. `react-native-esbuild-assets:/path/to/assets`
+ const splitted = path.resolve(buildContext.root, curr.path).split(':');
+ const actualPath = splitted[splitted.length - 1];
+ if (curr.original && !prev[curr.original]) {
+ logger.debug(
+ `${colors.gray(
+ `├─ ${stripRoot(buildContext.root, curr.original)} ▸`,
+ )} ${
+ isExternal(
+ buildContext.additionalData.externalPattern as string,
+ curr.original,
+ )
+ ? colors.gray('')
+ : stripRoot(buildContext.root, actualPath)
+ }`,
+ );
+ }
+ return {
+ ...prev,
+ ...(curr.original ? { [curr.original]: actualPath } : null),
+ };
+ },
+ {} as Record,
+ );
+
+ logger.debug(colors.gray(`╰─ ${Object.keys(importPaths).length} import(s)`));
+
+ return importPaths;
+};
+
+const stripRoot = (rootPath: string, path: string): string =>
+ path.replace(new RegExp(`^${rootPath}/?`), '');
+
+const isExternal = (pattern: string, path: string): boolean =>
+ new RegExp(pattern).test(path);
diff --git a/packages/hmr/lib/index.ts b/packages/hmr/lib/index.ts
new file mode 100644
index 00000000..93da20b8
--- /dev/null
+++ b/packages/hmr/lib/index.ts
@@ -0,0 +1,5 @@
+export * from './server';
+export * from './constants';
+export * from './helpers';
+export type * from './types';
+export type { HMRServer } from './server/HMRServer';
diff --git a/packages/hmr/lib/runtime/setup.ts b/packages/hmr/lib/runtime/setup.ts
new file mode 100644
index 00000000..995abc94
--- /dev/null
+++ b/packages/hmr/lib/runtime/setup.ts
@@ -0,0 +1,102 @@
+import type { ModuleId } from '@react-native-esbuild/shared';
+import * as RefreshRuntime from 'react-refresh/runtime';
+
+type HotModuleReplacementAcceptCallback = (payload: { id: ModuleId }) => void;
+type HotModuleReplacementDisposeCallback = () => void;
+
+if (__DEV__ && typeof global.__hmr === 'undefined') {
+ const HMR_DEBOUNCE_DELAY = 50;
+ let performReactRefreshTimeout: NodeJS.Timeout | null = null;
+
+ class HotModuleReplacementContext {
+ public static registry: Record<
+ ModuleId,
+ HotModuleReplacementContext | undefined
+ > = {};
+ public locked = false;
+ public acceptCallbacks: HotModuleReplacementAcceptCallback[] = [];
+ public disposeCallbacks: HotModuleReplacementDisposeCallback[] = [];
+
+ constructor(public id: ModuleId) {}
+
+ accept(acceptCallback: HotModuleReplacementAcceptCallback): void {
+ if (this.locked) return;
+ this.acceptCallbacks.push(acceptCallback);
+ }
+
+ dispose(disposeCallback: HotModuleReplacementDisposeCallback): void {
+ if (this.locked) return;
+ this.disposeCallbacks.push(disposeCallback);
+ }
+
+ lock(): void {
+ this.locked = true;
+ }
+ }
+
+ const HotModuleReplacementRuntimeModule: HotModuleReplacementRuntimeModule = {
+ register: (id: ModuleId) => {
+ const context = HotModuleReplacementContext.registry[id];
+ if (context) {
+ context.lock();
+ return context;
+ }
+ return (HotModuleReplacementContext.registry[id] =
+ new HotModuleReplacementContext(id));
+ },
+ update: (id: ModuleId, evalUpdates: () => void) => {
+ const context = HotModuleReplacementContext.registry[id];
+ if (context) {
+ context.disposeCallbacks.forEach((callback) => {
+ callback();
+ });
+ evalUpdates();
+ context.acceptCallbacks.forEach((callback) => {
+ callback({ id });
+ });
+ }
+ },
+ reactRefresh: {
+ register: RefreshRuntime.register,
+ getSignatureFunction: () =>
+ RefreshRuntime.createSignatureFunctionForTransform,
+ performReactRefresh: () => {
+ if (performReactRefreshTimeout !== null) {
+ return;
+ }
+
+ performReactRefreshTimeout = setTimeout(() => {
+ performReactRefreshTimeout = null;
+ }, HMR_DEBOUNCE_DELAY);
+
+ if (RefreshRuntime.hasUnrecoverableErrors()) {
+ console.error('[HMR::react-refresh] has unrecoverable errors');
+ return;
+ }
+ RefreshRuntime.performReactRefresh();
+ },
+ },
+ };
+
+ RefreshRuntime.injectIntoGlobalHook(global);
+
+ Object.defineProperty(global, '__hmr', {
+ enumerable: false,
+ value: HotModuleReplacementRuntimeModule,
+ });
+
+ // for web
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- allow
+ const isWeb = global?.navigator?.appName === 'Netscape';
+ if (isWeb) {
+ const socketURL = new URL('hot', `ws://${window.location.host}`);
+ const socket = new window.WebSocket(socketURL.href);
+ socket.addEventListener('message', (event) => {
+ const payload = window.JSON.parse(event.data);
+ if (payload.type === 'update') {
+ const code = payload.body?.added[0]?.module?.[1];
+ code && window.eval(code);
+ }
+ });
+ }
+}
diff --git a/packages/hmr/lib/server/HmrAppServer.ts b/packages/hmr/lib/server/HmrAppServer.ts
new file mode 100644
index 00000000..c5c7c19d
--- /dev/null
+++ b/packages/hmr/lib/server/HmrAppServer.ts
@@ -0,0 +1,53 @@
+import type { Server, MessageEvent, Data } from 'ws';
+import type { HMRClientMessage, HMRMessage, HMRMessageType } from '../types';
+import { HMRServer } from './HMRServer';
+
+export class HmrAppServer extends HMRServer {
+ private messageHandler?: (message: HMRClientMessage) => void;
+
+ constructor() {
+ super();
+ this.setup((socket) => {
+ socket.onmessage = this.handleMessage.bind(this);
+ });
+ }
+
+ private parseClientMessage(data: Data): HMRClientMessage | null {
+ try {
+ const parsedData = JSON.parse(String(data));
+ return 'type' in parsedData ? (parsedData as HMRClientMessage) : null;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ private handleMessage(event: MessageEvent): void {
+ const message = this.parseClientMessage(event.data);
+ if (!message) return;
+
+ /**
+ * @see {@link https://github.com/facebook/metro/blob/v0.77.0/packages/metro/src/HMRServer.js#L200-L239}
+ */
+ this.messageHandler?.(message);
+ }
+
+ public getWebSocketServer(): Server {
+ return this.server;
+ }
+
+ public setMessageHandler(handler: (message: HMRClientMessage) => void): void {
+ this.messageHandler = handler;
+ }
+
+ public send(
+ type: MessageType,
+ body: HMRMessage[MessageType],
+ ): void {
+ this.connectedSocket?.send(
+ JSON.stringify({
+ type,
+ body,
+ }),
+ );
+ }
+}
diff --git a/packages/hmr/lib/server/HmrServer.ts b/packages/hmr/lib/server/HmrServer.ts
new file mode 100644
index 00000000..7de92657
--- /dev/null
+++ b/packages/hmr/lib/server/HmrServer.ts
@@ -0,0 +1,40 @@
+import { Server, type WebSocket } from 'ws';
+import { logger } from '../shared';
+import type { HMRMessage, HMRMessageType } from '../types';
+
+export abstract class HMRServer {
+ protected server: Server;
+ protected connectedSocket?: WebSocket;
+
+ constructor() {
+ this.server = new Server({ noServer: true });
+ }
+
+ private handleClose(): void {
+ this.connectedSocket = undefined;
+ logger.debug('HMR web socket was closed');
+ }
+
+ public setup(onConnect?: (socket: WebSocket) => void): void {
+ this.server.on('connection', (socket) => {
+ this.connectedSocket = socket;
+ this.connectedSocket.onclose = this.handleClose.bind(this);
+ this.connectedSocket.onerror = this.handleClose.bind(this);
+ logger.debug('HMR web socket was connected');
+ onConnect?.(socket);
+ });
+
+ this.server.on('error', (error) => {
+ logger.error('HMR web socket server error', error);
+ });
+ }
+
+ public getWebSocketServer(): Server {
+ return this.server;
+ }
+
+ public abstract send(
+ type: MessageType,
+ body: HMRMessage[MessageType],
+ ): void;
+}
diff --git a/packages/hmr/lib/server/HmrWebServer.ts b/packages/hmr/lib/server/HmrWebServer.ts
new file mode 100644
index 00000000..f9ceb82e
--- /dev/null
+++ b/packages/hmr/lib/server/HmrWebServer.ts
@@ -0,0 +1,21 @@
+import type { HMRMessage, HMRMessageType } from '../types';
+import { HMRServer } from './HMRServer';
+
+export class HmrWebServer extends HMRServer {
+ constructor() {
+ super();
+ this.setup();
+ }
+
+ public send(
+ type: MessageType,
+ body: HMRMessage[MessageType],
+ ): void {
+ this.connectedSocket?.send(
+ JSON.stringify({
+ type,
+ body,
+ }),
+ );
+ }
+}
diff --git a/packages/hmr/lib/server/index.ts b/packages/hmr/lib/server/index.ts
new file mode 100644
index 00000000..b1bc45f4
--- /dev/null
+++ b/packages/hmr/lib/server/index.ts
@@ -0,0 +1,2 @@
+export * from './HMRAppServer';
+export * from './HMRWebServer';
diff --git a/packages/hmr/lib/shared.ts b/packages/hmr/lib/shared.ts
new file mode 100644
index 00000000..93cfbf63
--- /dev/null
+++ b/packages/hmr/lib/shared.ts
@@ -0,0 +1,3 @@
+import { Logger } from '@react-native-esbuild/shared';
+
+export const logger = new Logger('hmr');
diff --git a/packages/hmr/lib/types.ts b/packages/hmr/lib/types.ts
new file mode 100644
index 00000000..cf927f3a
--- /dev/null
+++ b/packages/hmr/lib/types.ts
@@ -0,0 +1,96 @@
+import type {
+ register,
+ createSignatureFunctionForTransform,
+} from 'react-refresh';
+import type { ModuleId } from '@react-native-esbuild/shared';
+
+/* eslint-disable no-var -- allow */
+declare global {
+ interface HotModuleReplacementRuntimeModule {
+ register: (id: ModuleId) => void;
+ update: (id: ModuleId, evalUpdates: () => void) => void;
+ // react-refresh/runtime
+ reactRefresh: {
+ register: typeof register;
+ getSignatureFunction: () => typeof createSignatureFunctionForTransform;
+ performReactRefresh: () => void;
+ };
+ }
+
+ // react-native
+ var __DEV__: boolean;
+
+ // react-refresh/runtime
+ var __hmr: HotModuleReplacementRuntimeModule | undefined;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- allow
+ var window: any;
+}
+
+/**
+ * HMR web socket messages
+ * @see {@link https://github.com/facebook/metro/blob/v0.77.0/packages/metro-runtime/src/modules/types.flow.js#L68}
+ */
+export type HMRClientMessage =
+ | RegisterEntryPointsMessage
+ | LogMessage
+ | LogOptInMessage;
+
+export interface RegisterEntryPointsMessage {
+ type: 'register-entrypoints';
+ entryPoints: string[];
+}
+
+export interface LogMessage {
+ type: 'log';
+ level:
+ | 'trace'
+ | 'info'
+ | 'warn'
+ | 'log'
+ | 'group'
+ | 'groupCollapsed'
+ | 'groupEnd'
+ | 'debug';
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- follow metro types
+ data: any[];
+ mode: 'BRIDGE' | 'NOBRIDGE';
+}
+
+export interface LogOptInMessage {
+ type: 'log-opt-in';
+}
+
+/**
+ * HMR update message
+ * @see {@link https://github.com/facebook/metro/blob/v0.77.0/packages/metro-runtime/src/modules/types.flow.js#L44-L56}
+ */
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- allow type
+export type HMRMessage = {
+ update: HMRUpdate;
+ 'update-start': {
+ isInitialUpdate: boolean;
+ };
+ 'update-done': undefined;
+};
+
+export type HMRMessageType = keyof HMRMessage;
+
+export interface HMRUpdate {
+ readonly added: HMRModule[];
+ readonly deleted: ModuleId[];
+ readonly modified: HMRModule[];
+ isInitialUpdate: boolean;
+ revisionId: string;
+}
+export interface HMRModule {
+ module: [ModuleId, string];
+ sourceMappingURL: string | null;
+ sourceURL: string | null;
+}
+
+export interface HMRTransformResult {
+ id: ModuleId;
+ code: string;
+ fullyReload: boolean;
+}
diff --git a/packages/config/package.json b/packages/hmr/package.json
similarity index 58%
rename from packages/config/package.json
rename to packages/hmr/package.json
index b6912dfe..b6fb5518 100644
--- a/packages/config/package.json
+++ b/packages/hmr/package.json
@@ -1,10 +1,12 @@
{
- "name": "@react-native-esbuild/config",
- "version": "0.1.0-beta.12",
- "description": "shared configs for @react-native-esbuild",
+ "name": "@react-native-esbuild/hmr",
+ "version": "0.1.0-beta.8",
+ "description": "HMR implementation for @react-native-esbuild",
"keywords": [
"react-native",
- "esbuild"
+ "esbuild",
+ "hmr",
+ "react-refresh"
],
"author": "leegeunhyeok ",
"homepage": "https://github.com/leegeunhyeok/react-native-esbuild#readme",
@@ -13,6 +15,10 @@
"module": "lib/index.ts",
"main": "dist/index.js",
"types": "dist/lib/index.d.ts",
+ "exports": {
+ ".": "./dist/index.js",
+ "./runtime": "./dist/runtime.js"
+ },
"directories": {
"lib": "lib",
"test": "__tests__"
@@ -29,7 +35,7 @@
"url": "git+https://github.com/leegeunhyeok/react-native-esbuild.git"
},
"scripts": {
- "prepack": "yarn cleanup && yarn build",
+ "prepack": "yarn build",
"cleanup": "rimraf ./dist",
"build": "node build/index.js && tsc"
},
@@ -37,6 +43,14 @@
"url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
},
"devDependencies": {
- "esbuild": "^0.19.5"
+ "@swc/helpers": "^0.5.2",
+ "@types/react-refresh": "^0.14.3",
+ "esbuild": "^0.19.3"
+ },
+ "dependencies": {
+ "@react-native-esbuild/internal": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
+ "react-refresh": "^0.14.0",
+ "ws": "^8.14.2"
}
}
diff --git a/packages/utils/tsconfig.json b/packages/hmr/tsconfig.json
similarity index 100%
rename from packages/utils/tsconfig.json
rename to packages/hmr/tsconfig.json
diff --git a/packages/internal/README.md b/packages/internal/README.md
index f56a534f..48e835b0 100644
--- a/packages/internal/README.md
+++ b/packages/internal/README.md
@@ -1,3 +1,3 @@
# `@react-native-esbuild/internal`
-> Internal configs and helpers for react-native
+> Internal(react-native) configs and helpers for @react-native-esbuild
diff --git a/packages/internal/lib/__tests__/__snapshots__/presets.test.ts.snap b/packages/internal/lib/__tests__/__snapshots__/presets.test.ts.snap
index ddf3b952..618b9261 100644
--- a/packages/internal/lib/__tests__/__snapshots__/presets.test.ts.snap
+++ b/packages/internal/lib/__tests__/__snapshots__/presets.test.ts.snap
@@ -1,7 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`getAssetRegistrationScript should match snapshot 1`] = `
-"
- module.exports = require('react-native/Libraries/Image/AssetRegistry').registerAsset({"__packager_asset":true,"name":"image","type":"png","scales":[1,2,3],"hash":"hash","httpServerLocation":"/image.png","width":0,"height":0});
- "
-`;
+exports[`getAssetRegistrationScript should match snapshot 1`] = `"module.exports =require('react-native/Libraries/Image/AssetRegistry').registerAsset({"__packager_asset":true,"name":"image","type":"png","scales":[1,2,3],"hash":"hash","httpServerLocation":"/image.png","width":0,"height":0});"`;
diff --git a/packages/internal/lib/__tests__/presets.test.ts b/packages/internal/lib/__tests__/presets.test.ts
index 1bd6162e..5dfbf3b1 100644
--- a/packages/internal/lib/__tests__/presets.test.ts
+++ b/packages/internal/lib/__tests__/presets.test.ts
@@ -1,4 +1,4 @@
-import { getAssetRegistrationScript } from '../presets';
+import { getAssetRegistrationScript } from '../helpers';
describe('getAssetRegistrationScript', () => {
let assetRegistrationScript: string;
diff --git a/packages/internal/lib/defaults.ts b/packages/internal/lib/constants.ts
similarity index 100%
rename from packages/internal/lib/defaults.ts
rename to packages/internal/lib/constants.ts
diff --git a/packages/internal/lib/helpers.ts b/packages/internal/lib/helpers.ts
index 364dfba7..0776f61f 100644
--- a/packages/internal/lib/helpers.ts
+++ b/packages/internal/lib/helpers.ts
@@ -1,5 +1,123 @@
+/* eslint-disable quotes -- Allow using backtick */
+import fs from 'node:fs/promises';
import path from 'node:path';
import indent from 'indent-string';
+import type { Asset } from './types';
+
+const REACT_NATIVE_GET_POLYFILLS_PATH = 'react-native/rn-get-polyfills';
+const REACT_NATIVE_INITIALIZE_CORE_PATH =
+ 'react-native/Libraries/Core/InitializeCore';
+
+const getNodeEnv = (dev: boolean): string =>
+ dev ? 'development' : 'production';
+
+/**
+ * @see {@link https://github.com/facebook/metro/blob/v0.78.0/packages/metro/src/lib/getPreludeCode.js}
+ */
+export const getInjectVariables = (dev: boolean): string[] => [
+ '__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()',
+ `__METRO_GLOBAL_PREFIX__=''`,
+ `__DEV__=${JSON.stringify(dev)}`,
+ 'process=this.process||{}',
+ `global = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this`,
+];
+
+const getReactNativePolyfills = (root: string): string[] => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires -- Allow dynamic require.
+ const getPolyfills = require(
+ resolveFromRoot(REACT_NATIVE_GET_POLYFILLS_PATH, root),
+ ) as () => string[];
+
+ return getPolyfills();
+};
+
+export const getReactNativeInitializeCore = (root: string): string => {
+ return require.resolve(
+ resolveFromRoot(REACT_NATIVE_INITIALIZE_CORE_PATH, root),
+ );
+};
+
+export const getGlobalVariables = (
+ dev: boolean,
+ platform: string,
+): Record => ({
+ __DEV__: JSON.stringify(dev),
+ global: platform === 'web' ? 'window' : 'global',
+ 'process.env.NODE_ENV': JSON.stringify(getNodeEnv(dev)),
+});
+
+export const getPreludeScript = async (
+ dev: boolean,
+ root: string,
+): Promise => {
+ const scripts = await Promise.all(
+ getReactNativePolyfills(root).map((scriptPath) =>
+ fs
+ .readFile(scriptPath, { encoding: 'utf-8' })
+ .then((code) => wrapWithIIFE(code, scriptPath)),
+ ),
+ );
+
+ const initialScripts = [
+ `var ${getInjectVariables(dev).join(',')};`,
+ `process.env=process.env||{};`,
+ `process.env.NODE_ENV=${JSON.stringify(getNodeEnv(dev))};`,
+ ...scripts,
+ ].join('\n');
+
+ return initialScripts;
+};
+
+/**
+ * Get asset registration script.
+ *
+ * @see {@link https://github.com/facebook/metro/blob/v0.78.0/packages/metro/src/Bundler/util.js#L29-L57}
+ * @see {@link https://github.com/facebook/react-native/blob/v0.72.0/packages/react-native/Libraries/Image/RelativeImageStub.js}
+ */
+export const getAssetRegistrationScript = ({
+ name,
+ type,
+ scales,
+ hash,
+ httpServerLocation,
+ dimensions,
+}: Pick<
+ Asset,
+ 'name' | 'type' | 'scales' | 'hash' | 'httpServerLocation' | 'dimensions'
+>): string => {
+ return `module.exports =require('react-native/Libraries/Image/AssetRegistry').registerAsset(${JSON.stringify(
+ {
+ __packager_asset: true,
+ name,
+ type,
+ scales,
+ hash,
+ httpServerLocation,
+ width: dimensions.width,
+ height: dimensions.height,
+ },
+ )});`;
+};
+
+/**
+ * Get reload script.
+ *
+ * @see turboModuleProxy {@link https://github.com/facebook/react-native/blob/v0.72.0/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js#L17}
+ * @see nativeModuleProxy {@link https://github.com/facebook/react-native/blob/v0.72.0/packages/react-native/Libraries/BatchedBridge/NativeModules.js#L179}
+ *
+ * ```ts
+ * // It works the same as the code below.
+ * import { DevSettings } from 'react-native';
+ *
+ * DevSettings.reload();
+ * ```
+ */
+export const getReloadByDevSettingsProxy = (): string => `(function () {
+ var moduleName = "DevSettings";
+ (global.__turboModuleProxy
+ ? global.__turboModuleProxy(moduleName)
+ : global.nativeModuleProxy[moduleName]).reload();
+})();`;
export const resolveFromRoot = (
targetPath: string,
@@ -16,7 +134,7 @@ export const resolveFromRoot = (
});
};
-export const wrapWithIIFE = (body: string, filepath: string): string => `
+const wrapWithIIFE = (body: string, filepath: string): string => `
// ${filepath}
(function (global) {
${indent(body, 2)}
diff --git a/packages/internal/lib/index.ts b/packages/internal/lib/index.ts
index 0d551471..79b36a72 100644
--- a/packages/internal/lib/index.ts
+++ b/packages/internal/lib/index.ts
@@ -1,3 +1,3 @@
-export * from './presets';
-export * from './defaults';
+export * from './constants';
+export * from './helpers';
export type * from './types';
diff --git a/packages/internal/lib/presets.ts b/packages/internal/lib/presets.ts
deleted file mode 100644
index 5cdd1b05..00000000
--- a/packages/internal/lib/presets.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-/* eslint-disable quotes -- Allow using backtick */
-import fs from 'node:fs/promises';
-import type { BundleOptions } from '@react-native-esbuild/config';
-import { resolveFromRoot, wrapWithIIFE } from './helpers';
-import type { Asset } from './types';
-
-const REACT_NATIVE_GET_POLYFILLS_PATH = 'react-native/rn-get-polyfills';
-const REACT_NATIVE_INITIALIZE_CORE_PATH =
- 'react-native/Libraries/Core/InitializeCore';
-
-const getNodeEnv = (dev: boolean): string =>
- dev ? 'development' : 'production';
-
-/**
- * @see {@link https://github.com/facebook/metro/blob/v0.78.0/packages/metro/src/lib/getPreludeCode.js}
- */
-export const getInjectVariables = (dev: boolean): string[] => [
- '__BUNDLE_START_TIME__=this.nativePerformanceNow?nativePerformanceNow():Date.now()',
- `__METRO_GLOBAL_PREFIX__=''`,
- `__DEV__=${JSON.stringify(dev)}`,
- 'process=this.process||{}',
- `global = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this`,
-];
-
-const getReactNativePolyfills = (root: string): Promise => {
- // eslint-disable-next-line @typescript-eslint/no-var-requires -- Allow dynamic require.
- const getPolyfills = require(
- resolveFromRoot(REACT_NATIVE_GET_POLYFILLS_PATH, root),
- ) as () => string[];
-
- return Promise.all(
- getPolyfills().map((scriptPath) =>
- fs
- .readFile(scriptPath, { encoding: 'utf-8' })
- .then((code) => wrapWithIIFE(code, scriptPath)),
- ),
- );
-};
-
-export const getReactNativeInitializeCore = (root: string): string => {
- return require.resolve(
- resolveFromRoot(REACT_NATIVE_INITIALIZE_CORE_PATH, root),
- );
-};
-
-export const getGlobalVariables = ({
- dev = true,
- platform,
-}: BundleOptions): Record => ({
- __DEV__: JSON.stringify(dev),
- global: platform === 'web' ? 'window' : 'global',
- 'process.env.NODE_ENV': JSON.stringify(getNodeEnv(dev)),
-});
-
-export const getPreludeScript = async (
- { dev = true }: BundleOptions,
- root: string,
-): Promise => {
- const polyfills = await getReactNativePolyfills(root);
- const initialScripts = [
- `var ${getInjectVariables(dev).join(',')};`,
- `process.env=process.env||{};`,
- `process.env.NODE_ENV=${JSON.stringify(getNodeEnv(dev))};`,
- ...polyfills,
- ].join('\n');
-
- return initialScripts;
-};
-/**
- * Get asset registration script.
- *
- * @see {@link https://github.com/facebook/metro/blob/v0.78.0/packages/metro/src/Bundler/util.js#L29-L57}
- * @see {@link https://github.com/facebook/react-native/blob/v0.72.0/packages/react-native/Libraries/Image/RelativeImageStub.js}
- */
-export const getAssetRegistrationScript = ({
- name,
- type,
- scales,
- hash,
- httpServerLocation,
- dimensions,
-}: Pick<
- Asset,
- 'name' | 'type' | 'scales' | 'hash' | 'httpServerLocation' | 'dimensions'
->): string => {
- return `
- module.exports = require('react-native/Libraries/Image/AssetRegistry').registerAsset(${JSON.stringify(
- {
- __packager_asset: true,
- name,
- type,
- scales,
- hash,
- httpServerLocation,
- width: dimensions.width,
- height: dimensions.height,
- },
- )});
- `;
-};
-
-/**
- * Get reload script.
- *
- * @see turboModuleProxy {@link https://github.com/facebook/react-native/blob/v0.72.0/packages/react-native/Libraries/TurboModule/TurboModuleRegistry.js#L17}
- * @see nativeModuleProxy {@link https://github.com/facebook/react-native/blob/v0.72.0/packages/react-native/Libraries/BatchedBridge/NativeModules.js#L179}
- *
- * ```ts
- * // It works the same as the code below.
- * import { DevSettings } from 'react-native';
- *
- * DevSettings.reload();
- * ```
- */
-export const getReloadByDevSettingsProxy = (): string => `(function () {
- var moduleName = "DevSettings";
- (global.__turboModuleProxy
- ? global.__turboModuleProxy(moduleName)
- : global.nativeModuleProxy[moduleName]).reload();
-})();`;
diff --git a/packages/internal/lib/types.ts b/packages/internal/lib/types.ts
index d4bf6e48..f2ca2b2b 100644
--- a/packages/internal/lib/types.ts
+++ b/packages/internal/lib/types.ts
@@ -1,5 +1,3 @@
-import type { BundlerSupportPlatform } from '@react-native-esbuild/config';
-
export interface Asset {
// `/path/to/asset/image.png`
path: string;
@@ -16,7 +14,34 @@ export interface Asset {
httpServerLocation: string;
hash: string;
dimensions: { width: number; height: number };
- platform: BundlerSupportPlatform | null;
+ platform: string | null;
}
export type AssetScale = 0.75 | 1 | 1.5 | 2 | 3;
+
+/**
+ * Event reportable event types
+ *
+ * @see {@link https://github.com/facebook/metro/blob/v0.78.0/packages/metro/src/lib/reporting.js#L36}
+ */
+export interface ClientLogEvent {
+ type: 'client_log';
+ level:
+ | 'trace'
+ | 'info'
+ | 'warn'
+ /**
+ * In react-native, ClientLogEvent['level'] does not defined `error` type.
+ * But, flipper supports the `error` type.
+ *
+ * @see {@link https://github.com/facebook/flipper/blob/v0.211.0/desktop/flipper-common/src/server-types.tsx#L76}
+ */
+ | 'error'
+ | 'log'
+ | 'group'
+ | 'groupCollapsed'
+ | 'groupEnd'
+ | 'debug';
+ data: unknown[];
+ mode: 'BRIDGE' | 'NOBRIDGE';
+}
diff --git a/packages/internal/package.json b/packages/internal/package.json
index 82c5e887..665a03c1 100644
--- a/packages/internal/package.json
+++ b/packages/internal/package.json
@@ -1,7 +1,7 @@
{
"name": "@react-native-esbuild/internal",
"version": "0.1.0-beta.12",
- "description": "shared configs and helpers for internal of react-native",
+ "description": "internal(react-native) configs and helpers for @react-native-esbuild",
"keywords": [
"react-native",
"esbuild"
@@ -37,7 +37,6 @@
"url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
},
"devDependencies": {
- "@react-native-esbuild/config": "workspace:*",
"esbuild": "^0.19.5"
},
"dependencies": {
diff --git a/packages/jest/lib/transformer/createTransformer.ts b/packages/jest/lib/transformer/createTransformer.ts
index a02d910e..c6f68245 100644
--- a/packages/jest/lib/transformer/createTransformer.ts
+++ b/packages/jest/lib/transformer/createTransformer.ts
@@ -10,20 +10,17 @@ import {
SyncTransformPipeline,
AsyncTransformPipeline,
swcPresets,
- type TransformerContext,
+ type BaseTransformContext,
} from '@react-native-esbuild/transformer';
import { getReactNativeInitializeCore } from '@react-native-esbuild/internal';
import type { TransformerConfig } from '../types';
import pkg from '../../package.json';
-const DUMMY_ESBUILD_VALUE = '';
const ROOT = process.cwd();
-const TRANSFORMER_CONTEXT: TransformerContext = {
+const BASE_TRANSFORM_CONTEXT: BaseTransformContext = {
root: ROOT,
- id: 0,
dev: true,
entry: '',
- path: '',
};
ReactNativeEsbuildBundler.bootstrap();
@@ -39,7 +36,7 @@ export const createTransformer = (config: TransformerConfig): Transformer => {
: undefined;
const syncTransformPipeline = new SyncTransformPipeline.builder(
- TRANSFORMER_CONTEXT,
+ BASE_TRANSFORM_CONTEXT,
)
.setSwcPreset(
swcPresets.getJestPreset({
@@ -59,7 +56,7 @@ export const createTransformer = (config: TransformerConfig): Transformer => {
.build();
const asyncTransformPipeline = new AsyncTransformPipeline.builder(
- TRANSFORMER_CONTEXT,
+ BASE_TRANSFORM_CONTEXT,
)
.setSwcPreset(
// Async transform is always ESM.
@@ -87,10 +84,9 @@ export const createTransformer = (config: TransformerConfig): Transformer => {
_options: TransformOptions,
): TransformedSource => {
const transformResult = syncTransformPipeline.transform(code, {
+ id: 0,
path,
- pluginData: DUMMY_ESBUILD_VALUE,
- namespace: DUMMY_ESBUILD_VALUE,
- suffix: DUMMY_ESBUILD_VALUE,
+ pluginData: {},
});
return { code: transformResult.code };
},
@@ -100,10 +96,9 @@ export const createTransformer = (config: TransformerConfig): Transformer => {
_options: TransformOptions,
): Promise => {
const transformResult = await asyncTransformPipeline.transform(code, {
+ id: 0,
path,
- pluginData: DUMMY_ESBUILD_VALUE,
- namespace: DUMMY_ESBUILD_VALUE,
- suffix: DUMMY_ESBUILD_VALUE,
+ pluginData: {},
});
return { code: transformResult.code };
},
diff --git a/packages/jest/package.json b/packages/jest/package.json
index c2cb7f67..128799d5 100644
--- a/packages/jest/package.json
+++ b/packages/jest/package.json
@@ -45,6 +45,7 @@
"@jest/create-cache-key-function": "^29.7.0",
"@react-native-esbuild/core": "workspace:*",
"@react-native-esbuild/internal": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"@react-native-esbuild/transformer": "workspace:*",
"md5": "^2.3.0"
},
diff --git a/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts b/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts
index 3a2dab34..3261e88f 100644
--- a/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts
@@ -2,11 +2,11 @@ import fs from 'node:fs/promises';
import deepmerge from 'deepmerge';
import { faker } from '@faker-js/faker';
import type { OnLoadArgs } from 'esbuild';
-import type { PluginContext } from '@react-native-esbuild/core';
import {
SUPPORT_PLATFORMS,
- type BundlerSupportPlatform,
-} from '@react-native-esbuild/config';
+ type BuildContext,
+ type SupportedPlatform,
+} from '@react-native-esbuild/shared';
import type { AssetScale } from '@react-native-esbuild/internal';
import {
getAssetPriority,
@@ -38,7 +38,7 @@ describe('assetRegisterPlugin', () => {
let filename: string;
let extension: string;
let scale: AssetScale;
- let platform: BundlerSupportPlatform;
+ let platform: SupportedPlatform;
beforeEach(() => {
filename = faker.string.alphanumeric(10);
@@ -72,7 +72,7 @@ describe('assetRegisterPlugin', () => {
let filename: string;
let extension: string;
let scale: AssetScale;
- let platform: BundlerSupportPlatform;
+ let platform: SupportedPlatform;
beforeEach(() => {
filename = faker.string.alphanumeric(10);
@@ -146,7 +146,7 @@ describe('assetRegisterPlugin', () => {
let filename: string;
let extension: string;
let scale: AssetScale;
- let platform: BundlerSupportPlatform;
+ let platform: SupportedPlatform;
beforeEach(() => {
filename = faker.string.alphanumeric(10);
@@ -241,7 +241,7 @@ describe('assetRegisterPlugin', () => {
});
describe('when `platform` is present', () => {
- let platform: BundlerSupportPlatform;
+ let platform: SupportedPlatform;
let suffixPathResult: SuffixPathResult;
beforeEach(() => {
@@ -281,7 +281,7 @@ describe('assetRegisterPlugin', () => {
});
describe('when both `platform` and `scale` are present', () => {
- let platform: BundlerSupportPlatform;
+ let platform: SupportedPlatform;
let scale: AssetScale;
let suffixPathResult: SuffixPathResult;
@@ -308,14 +308,16 @@ describe('assetRegisterPlugin', () => {
let filename: string;
let extension: string;
let pullPath: string;
- let mockedContext: PluginContext;
+ let mockedContext: BuildContext;
let mockedArgs: OnLoadArgs;
- const mockContext = (platform: BundlerSupportPlatform): void => {
- mockedContext = deepmerge(mockedContext, { platform } as PluginContext);
+ const mockContext = (platform: SupportedPlatform): void => {
+ mockedContext = deepmerge(mockedContext, {
+ bundleOptions: { platform },
+ } as BuildContext);
};
- const mockArgs = (platform: BundlerSupportPlatform | null): void => {
+ const mockArgs = (platform: SupportedPlatform | null): void => {
mockedArgs = {
path: pullPath,
pluginData: {
@@ -343,7 +345,7 @@ describe('assetRegisterPlugin', () => {
filename = faker.string.alphanumeric(10);
extension = faker.helpers.arrayElement(['.png', '.jpg', '.jpeg', '.gif']);
pullPath = `${dirname}/${filename}${extension}`;
- mockedContext = { root: faker.system.directoryPath() } as PluginContext;
+ mockedContext = { root: faker.system.directoryPath() } as BuildContext;
mockedArgs = {
path: pullPath,
pluginData: {
@@ -355,7 +357,7 @@ describe('assetRegisterPlugin', () => {
});
describe('when platform suffixed asset is exist', () => {
- let platform: BundlerSupportPlatform;
+ let platform: SupportedPlatform;
beforeEach(() => {
platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
diff --git a/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts b/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts
index b115bfda..084da130 100644
--- a/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts
@@ -1,11 +1,12 @@
import path from 'node:path';
import type { OnResolveArgs, ResolveResult } from 'esbuild';
-import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
+import { registerAsExternalModule } from '@react-native-esbuild/hmr';
+import type { PluginFactory } from '@react-native-esbuild/shared';
import {
+ ASSET_EXTENSIONS,
getAssetRegistrationScript,
type Asset,
} from '@react-native-esbuild/internal';
-import { ASSET_EXTENSIONS } from '@react-native-esbuild/internal';
import type { AssetRegisterPluginConfig, SuffixPathResult } from '../types';
import {
copyAssetsToDestination,
@@ -19,9 +20,9 @@ const DEFAULT_PLUGIN_CONFIG: AssetRegisterPluginConfig = {
assetExtensions: ASSET_EXTENSIONS,
};
-export const createAssetRegisterPlugin: ReactNativeEsbuildPluginCreator<
+export const createAssetRegisterPlugin: PluginFactory<
AssetRegisterPluginConfig
-> = (context, config = DEFAULT_PLUGIN_CONFIG) => ({
+> = (buildContext, config = DEFAULT_PLUGIN_CONFIG) => ({
name: NAME,
setup: (build) => {
const { assetExtensions = ASSET_EXTENSIONS } = config;
@@ -59,14 +60,14 @@ export const createAssetRegisterPlugin: ReactNativeEsbuildPluginCreator<
// 1
let suffixedPathResult = getSuffixedPath(args.path, {
scale: 1,
- platform: context.platform,
+ platform: buildContext.bundleOptions.platform,
});
let resolveResult = await resolveAsset(suffixedPathResult, args);
// 2
if (resolveResult.errors.length) {
suffixedPathResult = getSuffixedPath(args.path, {
- platform: context.platform,
+ platform: buildContext.bundleOptions.platform,
});
resolveResult = await resolveAsset(suffixedPathResult, args);
}
@@ -95,13 +96,21 @@ export const createAssetRegisterPlugin: ReactNativeEsbuildPluginCreator<
});
build.onLoad({ filter: /./, namespace: ASSET_NAMESPACE }, async (args) => {
- const asset = await resolveScaledAssets(context, args);
+ const asset = await resolveScaledAssets(buildContext, args);
+ const assetRegistrationScript = getAssetRegistrationScript(asset);
+ const moduleId = buildContext.moduleManager.getModuleId(args.path);
assets.push(asset);
return {
resolveDir: path.dirname(args.path),
- contents: getAssetRegistrationScript(asset),
+ contents: buildContext.hmrEnabled
+ ? registerAsExternalModule(
+ moduleId,
+ assetRegistrationScript,
+ 'module.exports',
+ )
+ : assetRegistrationScript,
loader: 'js',
};
});
@@ -109,7 +118,7 @@ export const createAssetRegisterPlugin: ReactNativeEsbuildPluginCreator<
build.onEnd(async (result) => {
// Skip copying assets when build failure.
if (result.errors.length) return;
- await copyAssetsToDestination(context, assets);
+ await copyAssetsToDestination(buildContext, assets);
});
},
});
diff --git a/packages/plugins/lib/assetRegisterPlugin/helpers/fs.ts b/packages/plugins/lib/assetRegisterPlugin/helpers/fs.ts
index 3bcb4c68..37019853 100644
--- a/packages/plugins/lib/assetRegisterPlugin/helpers/fs.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/helpers/fs.ts
@@ -1,6 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
-import type { PluginContext } from '@react-native-esbuild/core';
+import { BuildMode, type BuildContext } from '@react-native-esbuild/shared';
import type { Asset } from '@react-native-esbuild/internal';
import { logger } from '../../shared';
import {
@@ -15,11 +15,13 @@ import {
* @see {@link https://github.com/react-native-community/cli/blob/v11.3.6/packages/cli-plugin-metro/src/commands/bundle/assetPathUtils.ts}
*/
export const copyAssetsToDestination = async (
- context: PluginContext,
+ context: BuildContext,
assets: Asset[],
): Promise => {
- const { assetsDir, mode } = context;
- if (mode === 'watch') return;
+ const { mode } = context;
+ const { platform, assetsDir } = context.bundleOptions;
+
+ if (mode === BuildMode.Watch) return;
if (!assetsDir) {
logger.warn('asset destination is not set');
@@ -40,7 +42,7 @@ export const copyAssetsToDestination = async (
assets.map((asset): Promise => {
return Promise.all(
asset.scales.map(async (scale): Promise => {
- if (context.platform !== 'android') {
+ if (platform !== 'android') {
const from = await resolveAssetPath(asset, scale);
const to = path.join(
assetsDir,
diff --git a/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts b/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts
index cf129080..9d21f932 100644
--- a/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts
@@ -5,13 +5,13 @@ import type { OnLoadArgs } from 'esbuild';
import { imageSize } from 'image-size';
import md5 from 'md5';
import invariant from 'invariant';
-import type { PluginContext } from '@react-native-esbuild/core';
-import type { Asset, AssetScale } from '@react-native-esbuild/internal';
import {
ASSET_PATH,
SUPPORT_PLATFORMS,
- type BundlerSupportPlatform,
-} from '@react-native-esbuild/config';
+ type BuildContext,
+ type SupportedPlatform,
+} from '@react-native-esbuild/shared';
+import type { Asset, AssetScale } from '@react-native-esbuild/internal';
import type { SuffixPathResult } from '../../types';
const PLATFORM_SUFFIX_PATTERN = SUPPORT_PLATFORMS.map(
@@ -19,7 +19,7 @@ const PLATFORM_SUFFIX_PATTERN = SUPPORT_PLATFORMS.map(
).join('|');
const SCALE_PATTERN = '@(\\d+\\.?\\d*)x';
-const ALLOW_SCALES: Partial> = {
+const ALLOW_SCALES: Partial> = {
ios: [1, 2, 3],
};
@@ -54,7 +54,7 @@ export const addSuffix = (
basename: string,
extension: string,
options?: {
- platform?: BundlerSupportPlatform | null;
+ platform?: string | null;
scale?: string | number;
},
): string => {
@@ -93,7 +93,7 @@ export const getSuffixedPath = (
assetPath: string,
options?: {
scale?: AssetScale;
- platform?: BundlerSupportPlatform | null;
+ platform?: string | null;
},
): SuffixPathResult => {
// if `scale` present, append scale suffix to path
@@ -127,14 +127,13 @@ export const getDevServerBasePath = (asset: Asset): string => {
function assertSuffixPathResult(
data: OnLoadArgs['pluginData'],
): asserts data is SuffixPathResult {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- `pluginData` is any type.
invariant(data.basename, 'basename is empty');
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- `pluginData` is any type.
+
invariant(data.extension, 'extension is empty');
}
export const resolveScaledAssets = async (
- context: PluginContext,
+ context: BuildContext,
args: OnLoadArgs,
): Promise => {
assertSuffixPathResult(args.pluginData);
@@ -178,7 +177,9 @@ export const resolveScaledAssets = async (
.map(parseFloat)
.filter((scale: number) => {
// https://github.com/react-native-community/cli/blob/v11.3.6/packages/cli-plugin-metro/src/commands/bundle/filterPlatformAssetScales.ts
- return ALLOW_SCALES[context.platform]?.includes(scale) ?? true;
+ return (
+ ALLOW_SCALES[context.bundleOptions.platform]?.includes(scale) ?? true
+ );
})
.sort(),
httpServerLocation: path.join(ASSET_PATH, path.dirname(relativePath)),
diff --git a/packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts b/packages/plugins/lib/buildStatusPlugin/StatusLogger.ts
similarity index 89%
rename from packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts
rename to packages/plugins/lib/buildStatusPlugin/StatusLogger.ts
index 0a8c4384..5e1092f6 100644
--- a/packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts
+++ b/packages/plugins/lib/buildStatusPlugin/StatusLogger.ts
@@ -3,11 +3,16 @@ import path from 'node:path';
import { performance } from 'node:perf_hooks';
import esbuild, { type BuildResult, type Message } from 'esbuild';
import ora, { type Ora } from 'ora';
-import { getBuildStatusCachePath } from '@react-native-esbuild/config';
-import { colors, isTTY } from '@react-native-esbuild/utils';
-import { logger } from '../../../shared';
-import { ESBUILD_LABEL } from '../../logo';
-import type { BuildStatus, PluginContext } from '../../../types';
+import {
+ colors,
+ isTTY,
+ getBuildStatusCachePath,
+ BuildMode,
+ ESBUILD_LABEL,
+ type BuildStatus,
+ type BuildContext,
+} from '@react-native-esbuild/shared';
+import { logger } from '../shared';
import { fromTemplate, getSummaryTemplate } from './templates';
export class StatusLogger {
@@ -18,20 +23,23 @@ export class StatusLogger {
private loadedModules = 0;
private previousPercent = 0;
- constructor(private context: PluginContext) {
+ constructor(private context: BuildContext) {
this.platformText = colors.gray(
- `[${[context.platform, context.dev ? 'dev' : null]
+ `[${[
+ context.bundleOptions.platform,
+ context.bundleOptions.dev ? 'dev' : null,
+ ]
.filter(Boolean)
.join(', ')}]`,
);
this.spinner = ora({
color: 'yellow',
- discardStdin: context.mode === 'bundle',
+ discardStdin: context.mode === BuildMode.Bundle,
prefixText: colors.bgYellow(colors.black(ESBUILD_LABEL)),
});
}
- private statusUpdate(): void {
+ private updateStatus(): void {
const { resolved } = this.getStatus();
const loaded = this.loadedModules;
@@ -92,7 +100,7 @@ export class StatusLogger {
onLoad(): void {
++this.loadedModules;
- this.statusUpdate();
+ this.updateStatus();
}
setup(): void {
@@ -101,8 +109,9 @@ export class StatusLogger {
this.resolvedModules.clear();
this.loadedModules = 0;
this.previousPercent = 0;
- this.statusUpdate();
+ this.updateStatus();
+ process.stdout.write('\n');
isTTY()
? this.spinner.start()
: this.print(`${this.platformText} build in progress...`);
diff --git a/packages/core/lib/bundler/plugins/statusPlugin/statusPlugin.ts b/packages/plugins/lib/buildStatusPlugin/buildStatusPlugin.ts
similarity index 55%
rename from packages/core/lib/bundler/plugins/statusPlugin/statusPlugin.ts
rename to packages/plugins/lib/buildStatusPlugin/buildStatusPlugin.ts
index 61332838..899ea7df 100644
--- a/packages/core/lib/bundler/plugins/statusPlugin/statusPlugin.ts
+++ b/packages/plugins/lib/buildStatusPlugin/buildStatusPlugin.ts
@@ -1,29 +1,17 @@
import path from 'node:path';
import type { BuildResult } from 'esbuild';
-import type {
- ReactNativeEsbuildPluginCreator,
- PluginContext,
- BuildStatus,
-} from '../../../types';
+import type { PluginFactory } from '@react-native-esbuild/shared';
+import type { BuildStatusPluginConfig } from '../types';
import { StatusLogger } from './StatusLogger';
-const NAME = 'build-status-plugin';
-
-export const createBuildStatusPlugin: ReactNativeEsbuildPluginCreator<{
- onStart: (context: PluginContext) => void;
- onUpdate: (buildState: BuildStatus, context: PluginContext) => void;
- onEnd: (
- data: {
- result: BuildResult;
- success: boolean;
- },
- context: PluginContext,
- ) => void;
-}> = (context, config) => ({
- name: NAME,
+export const createBuildStatusPlugin: PluginFactory = (
+ buildContext,
+ config,
+) => ({
+ name: 'react-native-esbuild-bundler-status-plugin',
setup: (build): void => {
- const statusLogger = new StatusLogger(context);
const filter = /.*/;
+ const statusLogger = new StatusLogger(buildContext);
let statusLoaded = false;
build.onStart(async () => {
@@ -32,7 +20,7 @@ export const createBuildStatusPlugin: ReactNativeEsbuildPluginCreator<{
statusLoaded = true;
}
statusLogger.setup();
- config?.onStart(context);
+ config?.handler?.onBuildStart(buildContext);
});
build.onResolve({ filter }, (args) => {
@@ -40,7 +28,7 @@ export const createBuildStatusPlugin: ReactNativeEsbuildPluginCreator<{
statusLogger.onResolve(
isRelative ? path.resolve(args.resolveDir, args.path) : args.path,
);
- config?.onUpdate(statusLogger.getStatus(), context);
+ config?.handler?.onBuild(buildContext, statusLogger.getStatus());
return null;
});
@@ -52,7 +40,11 @@ export const createBuildStatusPlugin: ReactNativeEsbuildPluginCreator<{
build.onEnd(async (result: BuildResult) => {
const success = await statusLogger.summary(result);
await statusLogger.persistStatus();
- config?.onEnd({ result, success }, context);
+ config?.handler?.onBuildEnd(
+ buildContext,
+ { result, success },
+ statusLogger.getStatus(),
+ );
});
},
});
diff --git a/packages/plugins/lib/buildStatusPlugin/index.ts b/packages/plugins/lib/buildStatusPlugin/index.ts
new file mode 100644
index 00000000..135f4064
--- /dev/null
+++ b/packages/plugins/lib/buildStatusPlugin/index.ts
@@ -0,0 +1 @@
+export { createBuildStatusPlugin } from './buildStatusPlugin';
diff --git a/packages/core/lib/bundler/plugins/statusPlugin/templates.ts b/packages/plugins/lib/buildStatusPlugin/templates.ts
similarity index 91%
rename from packages/core/lib/bundler/plugins/statusPlugin/templates.ts
rename to packages/plugins/lib/buildStatusPlugin/templates.ts
index 50407223..504c24c3 100644
--- a/packages/core/lib/bundler/plugins/statusPlugin/templates.ts
+++ b/packages/plugins/lib/buildStatusPlugin/templates.ts
@@ -1,4 +1,4 @@
-import { isTTY, colors } from '@react-native-esbuild/utils';
+import { colors, isTTY } from '@react-native-esbuild/shared';
const summaryTemplateForTTY = `
╭───────────╯
diff --git a/packages/plugins/lib/index.ts b/packages/plugins/lib/index.ts
index cee87ec3..3a93c85a 100644
--- a/packages/plugins/lib/index.ts
+++ b/packages/plugins/lib/index.ts
@@ -1,4 +1,6 @@
export * from './assetRegisterPlugin';
+export * from './buildStatusPlugin';
+export * from './metafilePlugin';
export * from './reactNativeRuntimeTransformPlugin';
export * from './reactNativeWebPlugin';
export * from './svgTransformPlugin';
diff --git a/packages/core/lib/bundler/plugins/metafilePlugin/index.ts b/packages/plugins/lib/metafilePlugin/index.ts
similarity index 100%
rename from packages/core/lib/bundler/plugins/metafilePlugin/index.ts
rename to packages/plugins/lib/metafilePlugin/index.ts
diff --git a/packages/plugins/lib/metafilePlugin/metafilePlugin.ts b/packages/plugins/lib/metafilePlugin/metafilePlugin.ts
new file mode 100644
index 00000000..e7e7cb77
--- /dev/null
+++ b/packages/plugins/lib/metafilePlugin/metafilePlugin.ts
@@ -0,0 +1,26 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import type { BuildResult } from 'esbuild';
+import type { PluginFactory } from '@react-native-esbuild/shared';
+import { logger } from '../shared';
+
+export const createMetafilePlugin: PluginFactory = (buildContext) => ({
+ name: 'react-native-esbuild-metafile-plugin',
+ setup: (build): void => {
+ build.onEnd(async (result: BuildResult) => {
+ if (!(buildContext.bundleOptions.metafile && result.metafile)) return;
+
+ const filename = path.join(
+ buildContext.root,
+ `metafile-${buildContext.bundleOptions.platform}-${new Date()
+ .getTime()
+ .toString()}.json`,
+ );
+ logger.debug('writing esbuild metafile', { destination: filename });
+
+ await fs.writeFile(filename, JSON.stringify(result.metafile), {
+ encoding: 'utf-8',
+ });
+ });
+ },
+});
diff --git a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/caches.ts b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/caches.ts
index 0a9aedf3..2e9be2df 100644
--- a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/caches.ts
+++ b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/caches.ts
@@ -1,40 +1,113 @@
-import type { CacheController } from '@react-native-esbuild/core';
-import type { CacheConfig } from '../../types';
+import type { AsyncTransformStep } from '@react-native-esbuild/transformer';
+import {
+ getCacheKey,
+ type BuildContext,
+ type CacheStorage,
+} from '@react-native-esbuild/shared';
+
+export const getCachingSteps = (
+ buildContext: BuildContext,
+): {
+ beforeTransform: AsyncTransformStep;
+ afterTransform: AsyncTransformStep;
+} => {
+ const findCacheBeforeTransform: AsyncTransformStep = async (
+ code,
+ context,
+ ) => {
+ /**
+ * 1. Use previous transformed result and skip transform
+ * when file is not changed and transform result exist in memory.
+ */
+ const inMemoryCache = getTransformedCodeFromInMemoryCache(
+ buildContext.cacheStorage,
+ context.path,
+ context.pluginData.mtimeMs,
+ );
+ if (inMemoryCache) {
+ return { code: inMemoryCache, done: true };
+ }
+
+ // 2. Transform code on each build task when cache is disabled.
+ if (!buildContext.config.cache) {
+ return { code, done: false };
+ }
+
+ // 3. Trying to get cache from file system.
+ // = cache exist ? use cache : transform code
+ const cachedCode = await getTransformedCodeFromFileSystemCache(
+ buildContext.cacheStorage,
+ (context.pluginData.hash = getCacheKey(
+ buildContext.id,
+ context.path,
+ context.pluginData.mtimeMs,
+ )),
+ );
+
+ return { code: cachedCode ?? code, done: Boolean(cachedCode) };
+ };
+
+ const writeCacheAfterTransform: AsyncTransformStep = async (
+ code,
+ context,
+ ) => {
+ writeTransformedCodeToInMemoryCache(
+ buildContext.cacheStorage,
+ code,
+ context.path,
+ context.pluginData.mtimeMs,
+ );
+
+ if (buildContext.config.cache) {
+ await writeTransformedCodeToFileSystemCache(
+ buildContext.cacheStorage,
+ code,
+ context.pluginData.hash,
+ );
+ }
+
+ return { code, done: true };
+ };
+
+ return {
+ beforeTransform: findCacheBeforeTransform,
+ afterTransform: writeCacheAfterTransform,
+ };
+};
export const getTransformedCodeFromInMemoryCache = (
- controller: CacheController,
- cacheConfig: CacheConfig,
+ transformCacheStorage: CacheStorage,
+ filePath: string,
+ mtimeMs: number,
): string | null => {
- const inMemoryCache = controller.readFromMemory(cacheConfig.hash);
+ const inMemoryCache = transformCacheStorage.readFromMemory(filePath);
// If file is not modified, use cache data instead.
- return inMemoryCache && inMemoryCache.modifiedAt === cacheConfig.mtimeMs
+ return inMemoryCache && inMemoryCache.mtimeMs === mtimeMs
? inMemoryCache.data
: null;
};
export const getTransformedCodeFromFileSystemCache = async (
- controller: CacheController,
- cacheConfig: CacheConfig,
+ transformCacheStorage: CacheStorage,
+ key: string,
): Promise => {
- const fsCache = await controller.readFromFileSystem(cacheConfig.hash);
- return fsCache ?? null;
+ const fileCache = await transformCacheStorage.readFromFileSystem(key);
+ return fileCache ?? null;
};
export const writeTransformedCodeToInMemoryCache = (
- controller: CacheController,
- code: string,
- cacheConfig: CacheConfig,
+ transformCacheStorage: CacheStorage,
+ data: string,
+ key: string,
+ mtimeMs: number,
): void => {
- controller.writeToMemory(cacheConfig.hash, {
- data: code,
- modifiedAt: cacheConfig.mtimeMs,
- });
+ transformCacheStorage.writeToMemory(key, { data, mtimeMs });
};
export const writeTransformedCodeToFileSystemCache = (
- controller: CacheController,
- code: string,
- cacheConfig: CacheConfig,
+ transformCacheStorage: CacheStorage,
+ data: string,
+ key: string,
): Promise => {
- return controller.writeToFileSystem(cacheConfig.hash, code);
+ return transformCacheStorage.writeToFileSystem(key, data);
};
diff --git a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/index.ts b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/index.ts
index c9314409..d4fdc5e5 100644
--- a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/index.ts
+++ b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/index.ts
@@ -1 +1,2 @@
export * from './caches';
+export * from './json';
diff --git a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/json.ts b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/json.ts
new file mode 100644
index 00000000..378c202a
--- /dev/null
+++ b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/json.ts
@@ -0,0 +1,38 @@
+import fs from 'node:fs/promises';
+import type { PluginBuild } from 'esbuild';
+import { registerAsExternalModule } from '@react-native-esbuild/hmr';
+import type { BuildContext } from '@react-native-esbuild/shared';
+
+/**
+ * When development mode + HMR enabled, the '.json' contents
+ * must be registered in the global module registry for HMR.
+ */
+export const transformJsonAsJsModule = (
+ buildContext: BuildContext,
+ build: PluginBuild,
+): void => {
+ if (!buildContext.hmrEnabled) return;
+
+ build.onLoad({ filter: /\.json$/ }, async (args) => {
+ const moduleId = buildContext.moduleManager.getModuleId(args.path);
+ const rawJson = await fs.readFile(args.path, { encoding: 'utf-8' });
+ const parsedJson = JSON.parse(rawJson) as Record;
+ const identifier = 'json';
+
+ return {
+ contents: registerAsExternalModule(
+ moduleId,
+ `const ${identifier} = ${rawJson};
+ ${Object.keys(parsedJson)
+ .map((member) => {
+ const memberName = JSON.stringify(member);
+ return `exports[${memberName}] = ${identifier}[${memberName}]`;
+ })
+ .join('\n')}
+ module.exports = ${identifier};`,
+ identifier,
+ ),
+ loader: 'js',
+ };
+ });
+};
diff --git a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/reactNativeRuntimeTransformPlugin.ts b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/reactNativeRuntimeTransformPlugin.ts
index 6b9d3026..7de75d84 100644
--- a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/reactNativeRuntimeTransformPlugin.ts
+++ b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/reactNativeRuntimeTransformPlugin.ts
@@ -1,144 +1,71 @@
import fs from 'node:fs/promises';
import path from 'node:path';
-import type { OnLoadResult } from 'esbuild';
-import {
- ReactNativeEsbuildBundler as Bundler,
- type ReactNativeEsbuildPluginCreator,
-} from '@react-native-esbuild/core';
-import { getReactNativeInitializeCore } from '@react-native-esbuild/internal';
-import {
- AsyncTransformPipeline,
- swcPresets,
- type AsyncTransformStep,
-} from '@react-native-esbuild/transformer';
+import { getCommonReactNativeRuntimePipelineBuilder } from '@react-native-esbuild/transformer';
+import type { PluginFactory } from '@react-native-esbuild/shared';
import { logger } from '../shared';
-import type { ReactNativeRuntimeTransformPluginConfig } from '../types';
-import {
- getTransformedCodeFromInMemoryCache,
- getTransformedCodeFromFileSystemCache,
- writeTransformedCodeToInMemoryCache,
- writeTransformedCodeToFileSystemCache,
-} from './helpers';
+import { getCachingSteps, transformJsonAsJsModule } from './helpers';
-const NAME = 'react-native-runtime-transform-plugin';
-
-export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCreator<
- ReactNativeRuntimeTransformPluginConfig
-> = (context, config) => ({
- name: NAME,
+export const createReactNativeRuntimeTransformPlugin: PluginFactory = (
+ buildContext,
+) => ({
+ name: 'react-native-runtime-transform-plugin',
setup: (build): void => {
- const cacheController = Bundler.caches.get(context.id);
- const bundlerSharedData = Bundler.shared.get(context.id);
- const cacheEnabled = context.config.cache ?? true;
- const {
- stripFlowPackageNames = [],
- fullyTransformPackageNames = [],
- additionalTransformRules,
- } = context.config.transformer ?? {};
- const additionalBabelRules = additionalTransformRules?.babel ?? [];
- const additionalSwcRules = additionalTransformRules?.swc ?? [];
- const injectScriptPaths = [
- getReactNativeInitializeCore(context.root),
- ...(config?.injectScriptPaths ?? []),
- ];
-
- const reactNativeRuntimePreset = swcPresets.getReactNativeRuntimePreset(
- context.config.transformer?.jsc,
+ const filter = /\.(?:[mc]js|[tj]sx?)$/;
+ const cachingSteps = getCachingSteps(buildContext);
+ const transformPipeline = getCommonReactNativeRuntimePipelineBuilder(
+ buildContext,
+ )
+ .beforeTransform(cachingSteps.beforeTransform)
+ .afterTransform(cachingSteps.afterTransform)
+ .build();
+
+ transformJsonAsJsModule(buildContext, build);
+
+ build.onResolve({ filter }, (args) =>
+ args.kind === 'entry-point'
+ ? {
+ path: args.path,
+ pluginData: { isEntryPoint: true },
+ }
+ : null,
);
- const onBeforeTransform: AsyncTransformStep = async (
- code,
- args,
- moduleMeta,
- ) => {
- const isChangedFile = bundlerSharedData.watcher.changed === args.path;
- const cacheConfig = {
- hash: moduleMeta.hash,
- mtimeMs: moduleMeta.stats.mtimeMs,
- };
-
- // 1. Force re-transform when file is changed.
- if (isChangedFile) {
- logger.debug('changed file detected', { path: args.path });
- return { code, done: false };
- }
-
- /**
- * 2. Use previous transformed result and skip transform
- * when file is not changed and transform result exist in memory.
- */
- const inMemoryCache = getTransformedCodeFromInMemoryCache(
- cacheController,
- cacheConfig,
- );
- if (inMemoryCache) {
- return { code: inMemoryCache, done: true };
+ build.onLoad({ filter }, async (args) => {
+ const moduleId = buildContext.moduleManager.getModuleId(args.path);
+ let handle: fs.FileHandle | undefined;
+
+ try {
+ handle = await fs.open(args.path);
+ const stat = await handle.stat();
+ const rawCode = await handle.readFile({ encoding: 'utf-8' });
+
+ const { code } = await transformPipeline.transform(rawCode, {
+ id: moduleId,
+ path: args.path,
+ pluginData: {
+ ...args.pluginData,
+ mtimeMs: stat.mtimeMs,
+ externalPattern: buildContext.additionalData.externalPattern,
+ },
+ });
+
+ return { contents: code, loader: 'js' };
+ } finally {
+ try {
+ await handle?.close();
+ } catch (error) {
+ logger.error('unexpected error', error);
+ process.exit(1);
+ }
}
-
- // 3. Transform code on each build task when cache is disabled.
- if (!cacheEnabled) {
- return { code, done: false };
- }
-
- // 4. Trying to get cache from file system.
- // = cache exist ? use cache : transform code
- const cachedCode = await getTransformedCodeFromFileSystemCache(
- cacheController,
- cacheConfig,
- );
-
- return { code: cachedCode ?? code, done: Boolean(cachedCode) };
- };
-
- const onAfterTransform: AsyncTransformStep = async (
- code,
- _args,
- moduleMeta,
- ) => {
- const cacheConfig = {
- hash: moduleMeta.hash,
- mtimeMs: moduleMeta.stats.mtimeMs,
- };
- writeTransformedCodeToInMemoryCache(cacheController, code, cacheConfig);
-
- if (cacheEnabled) {
- await writeTransformedCodeToFileSystemCache(
- cacheController,
- code,
- cacheConfig,
- );
- }
-
- return { code, done: true };
- };
-
- let transformPipeline: AsyncTransformPipeline;
- const transformPipelineBuilder = new AsyncTransformPipeline.builder(context)
- .setSwcPreset(reactNativeRuntimePreset)
- .setInjectScripts(injectScriptPaths)
- .setFullyTransformPackages(fullyTransformPackageNames)
- .setStripFlowPackages(stripFlowPackageNames)
- .setAdditionalBabelTransformRules(additionalBabelRules)
- .setAdditionalSwcTransformRules(additionalSwcRules)
- .onStart(onBeforeTransform)
- .onEnd(onAfterTransform);
-
- build.onStart(() => {
- transformPipeline = transformPipelineBuilder.build();
- });
-
- build.onLoad({ filter: /\.(?:[mc]js|[tj]sx?)$/ }, async (args) => {
- const rawCode = await fs.readFile(args.path, { encoding: 'utf-8' });
- return {
- contents: (await transformPipeline.transform(rawCode, args)).code,
- loader: 'js',
- } as OnLoadResult;
});
build.onEnd(async (args) => {
if (args.errors.length) return;
- if (!(build.initialOptions.outfile && context.sourcemap)) {
+ if (
+ !(build.initialOptions.outfile && buildContext.bundleOptions.sourcemap)
+ ) {
logger.debug('outfile or sourcemap path is not specified');
return;
}
@@ -149,10 +76,10 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
logger.debug('move sourcemap to specified path', {
from: sourceMapPath,
- to: context.sourcemap,
+ to: buildContext.bundleOptions.sourcemap,
});
- await fs.rename(sourceMapPath, context.sourcemap);
+ await fs.rename(sourceMapPath, buildContext.bundleOptions.sourcemap);
});
},
});
diff --git a/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts b/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts
index 71fb6d72..d50e483b 100644
--- a/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts
+++ b/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts
@@ -1,11 +1,13 @@
import path from 'node:path';
import type { OnResolveArgs, ResolveResult } from 'esbuild';
-import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
-import { getDevServerPublicPath } from '@react-native-esbuild/config';
+import {
+ BuildMode,
+ getDevServerPublicPath,
+ type PluginFactory,
+} from '@react-native-esbuild/shared';
import { logger } from '../shared';
import { generateIndexPage } from './helpers';
-const NAME = 'react-native-web-plugin';
const RESOLVE_PATTERNS = [
// For relative path import of initializeCore.
/node_modules\/react-native\//,
@@ -13,16 +15,17 @@ const RESOLVE_PATTERNS = [
/^react-native$/,
];
-export const createReactNativeWebPlugin: ReactNativeEsbuildPluginCreator = (
- context,
-) => ({
- name: NAME,
+export const createReactNativeWebPlugin: PluginFactory = (buildContext) => ({
+ name: 'react-native-web-plugin',
setup: (build): void => {
- const { root, platform, outfile, mode } = context;
- const { template, placeholders } = context.config.web ?? {};
- const destination =
- mode === 'watch' ? getDevServerPublicPath(root) : path.dirname(outfile);
+ const { root, mode } = buildContext;
+ const { platform, outfile } = buildContext.bundleOptions;
+ const { template, placeholders } = buildContext.config.web ?? {};
const bundleFilename = path.basename(outfile);
+ const destination =
+ mode === BuildMode.Watch
+ ? getDevServerPublicPath(root)
+ : path.dirname(outfile);
if (platform !== 'web') return;
diff --git a/packages/plugins/lib/shared.ts b/packages/plugins/lib/shared.ts
index 2c5a342e..72e2144f 100644
--- a/packages/plugins/lib/shared.ts
+++ b/packages/plugins/lib/shared.ts
@@ -1,3 +1,3 @@
-import { Logger } from '@react-native-esbuild/utils';
+import { Logger } from '@react-native-esbuild/shared';
export const logger = new Logger('plugins');
diff --git a/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts b/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts
index 41005e78..fde974c6 100644
--- a/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts
+++ b/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts
@@ -1,27 +1,37 @@
import fs from 'node:fs/promises';
import { transform } from '@svgr/core';
-import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
+import { registerAsExternalModule } from '@react-native-esbuild/hmr';
+import type { PluginFactory } from '@react-native-esbuild/shared';
+import { defaultTemplate, SVG_COMPONENT_NAME } from './templates';
-const NAME = 'svg-transform-plugin';
-
-export const createSvgTransformPlugin: ReactNativeEsbuildPluginCreator = (
- context,
-) => ({
- name: NAME,
+export const createSvgTransformPlugin: PluginFactory = (buildContext) => ({
+ name: 'react-native-esbuild-svg-transform-plugin',
setup: (build): void => {
- const isNative = ['android', 'ios'].includes(context.platform);
+ const isNative = ['android', 'ios'].includes(
+ buildContext.bundleOptions.platform,
+ );
build.onLoad({ filter: /\.svg$/ }, async (args) => {
+ const moduleId = buildContext.moduleManager.getModuleId(args.path);
const rawSvg = await fs.readFile(args.path, { encoding: 'utf8' });
+ const svgTransformedCode = await transform(
+ rawSvg,
+ {
+ template: defaultTemplate,
+ plugins: ['@svgr/plugin-jsx'],
+ native: isNative,
+ },
+ { filePath: args.path },
+ );
+
return {
- contents: await transform(
- rawSvg,
- {
- plugins: ['@svgr/plugin-jsx'],
- native: isNative,
- },
- { filePath: args.path },
- ),
+ contents: buildContext.hmrEnabled
+ ? registerAsExternalModule(
+ moduleId,
+ svgTransformedCode,
+ SVG_COMPONENT_NAME,
+ )
+ : svgTransformedCode,
loader: 'jsx',
};
});
diff --git a/packages/plugins/lib/svgTransformPlugin/templates/index.ts b/packages/plugins/lib/svgTransformPlugin/templates/index.ts
new file mode 100644
index 00000000..c4ce035f
--- /dev/null
+++ b/packages/plugins/lib/svgTransformPlugin/templates/index.ts
@@ -0,0 +1,15 @@
+import type { Config } from '@svgr/core';
+
+export const SVG_COMPONENT_NAME = 'SvgLogo';
+
+export const defaultTemplate: Config['template'] = (variables, { tpl }) => {
+ return tpl`${variables.imports};
+
+${variables.interfaces};
+
+const ${SVG_COMPONENT_NAME} = (${variables.props}) => (
+ ${variables.jsx}
+);
+
+export default ${SVG_COMPONENT_NAME};`;
+};
diff --git a/packages/plugins/lib/types.ts b/packages/plugins/lib/types.ts
index 5b837962..02edfe84 100644
--- a/packages/plugins/lib/types.ts
+++ b/packages/plugins/lib/types.ts
@@ -1,6 +1,6 @@
-import type { BundlerSupportPlatform } from '@react-native-esbuild/config';
+import type { BuildStatusListener } from '@react-native-esbuild/shared';
-// asset-register-plugin
+// assetRegisterPlugin
export interface AssetRegisterPluginConfig {
assetExtensions?: string[];
}
@@ -10,15 +10,10 @@ export interface SuffixPathResult {
dirname: string;
basename: string;
extension: string;
- platform: BundlerSupportPlatform | null;
+ platform: string | null;
}
-// react-native-runtime-transform-plugin
-export interface ReactNativeRuntimeTransformPluginConfig {
- injectScriptPaths?: string[];
-}
-
-export interface CacheConfig {
- hash: string;
- mtimeMs: number;
+// buildStatusPlugin
+export interface BuildStatusPluginConfig {
+ handler?: BuildStatusListener;
}
diff --git a/packages/plugins/package.json b/packages/plugins/package.json
index aac9060c..317bce59 100644
--- a/packages/plugins/package.json
+++ b/packages/plugins/package.json
@@ -44,15 +44,16 @@
"esbuild": "^0.19.5"
},
"dependencies": {
- "@react-native-esbuild/config": "workspace:*",
- "@react-native-esbuild/core": "workspace:*",
+ "@react-native-esbuild/hmr": "workspace:*",
"@react-native-esbuild/internal": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"@react-native-esbuild/transformer": "workspace:*",
- "@react-native-esbuild/utils": "workspace:*",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
+ "esbuild-plugin-module-id": "^0.1.3",
"image-size": "^1.0.2",
"invariant": "^2.2.4",
- "md5": "^2.3.0"
+ "md5": "^2.3.0",
+ "ora": "^5.4.1"
}
}
diff --git a/packages/plugins/tsconfig.json b/packages/plugins/tsconfig.json
index 3d3bb195..bd42e358 100644
--- a/packages/plugins/tsconfig.json
+++ b/packages/plugins/tsconfig.json
@@ -6,6 +6,6 @@
"declaration": true,
"emitDeclarationOnly": true
},
- "include": ["./lib", "../core/lib/transformers/babel.ts"],
+ "include": ["./lib"],
"exclude": ["./dist"]
}
diff --git a/packages/utils/CHANGELOG.md b/packages/shared/CHANGELOG.md
similarity index 100%
rename from packages/utils/CHANGELOG.md
rename to packages/shared/CHANGELOG.md
diff --git a/packages/utils/README.md b/packages/shared/README.md
similarity index 100%
rename from packages/utils/README.md
rename to packages/shared/README.md
diff --git a/packages/config/build/index.js b/packages/shared/build/index.js
similarity index 100%
rename from packages/config/build/index.js
rename to packages/shared/build/index.js
diff --git a/packages/utils/lib/__tests__/logger.test.ts b/packages/shared/lib/__tests__/logger.test.ts
similarity index 100%
rename from packages/utils/lib/__tests__/logger.test.ts
rename to packages/shared/lib/__tests__/logger.test.ts
diff --git a/packages/shared/lib/constants.ts b/packages/shared/lib/constants.ts
new file mode 100644
index 00000000..13b4d9fb
--- /dev/null
+++ b/packages/shared/lib/constants.ts
@@ -0,0 +1,15 @@
+import path from 'node:path';
+import os from 'node:os';
+
+export const DEFAULT_ENTRY_POINT = 'index.js';
+export const DEFAULT_WEB_ENTRY_POINT = 'index.web.js';
+export const DEFAULT_OUTFILE = 'main.jsbundle';
+
+export const LOCAL_CACHE_DIR = '.rne';
+export const GLOBAL_CACHE_DIR = path.join(os.tmpdir(), 'react-native-esbuild');
+
+export const SUPPORT_PLATFORMS = ['android', 'ios', 'web'] as const;
+
+export const ASSET_PATH = 'assets';
+export const PUBLIC_PATH = 'public';
+export const STATUS_CACHE_FILE = 'build-status.json';
diff --git a/packages/shared/lib/enums.ts b/packages/shared/lib/enums.ts
new file mode 100644
index 00000000..86f4e94d
--- /dev/null
+++ b/packages/shared/lib/enums.ts
@@ -0,0 +1,16 @@
+/**
+ * Flags for `BundleOptions`
+ */
+export enum OptionFlag {
+ None = 0b00000000,
+ PlatformAndroid = 0b00000001,
+ PlatformIos = 0b00000010,
+ PlatformWeb = 0b00000100,
+ Dev = 0b00001000,
+ Minify = 0b00010000,
+}
+
+export enum BuildMode {
+ Bundle,
+ Watch,
+}
diff --git a/packages/config/lib/common/core.ts b/packages/shared/lib/helpers.ts
similarity index 72%
rename from packages/config/lib/common/core.ts
rename to packages/shared/lib/helpers.ts
index bc24da2c..24ddb5aa 100644
--- a/packages/config/lib/common/core.ts
+++ b/packages/shared/lib/helpers.ts
@@ -1,10 +1,14 @@
import path from 'node:path';
+import md5 from 'md5';
+import { OptionFlag } from './enums';
+import type { BundleOptions, Id } from './types/core';
import {
DEFAULT_ENTRY_POINT,
DEFAULT_OUTFILE,
LOCAL_CACHE_DIR,
-} from '../shares';
-import { OptionFlag, type BundleOptions } from '../types';
+ PUBLIC_PATH,
+ STATUS_CACHE_FILE,
+} from './constants';
export const combineWithDefaultBundleOptions = (
options: Partial,
@@ -43,6 +47,27 @@ export const getIdByOptions = ({
return value;
};
+/**
+ * Generate hash that contains the file path, modification time, and bundle options.
+ *
+ * `id` is combined(platform, dev, minify) bundle options value in `@react-native-esbuild`.
+ *
+ * hash = md5(id + modified time + file path + core version)
+ * number + number + string + string
+ *
+ * @param id - id
+ * @param filepath - file path
+ * @param modifiedAt - file modified at timestamp
+ * @returns
+ */
+export const getCacheKey = (
+ id: Id,
+ filepath: string,
+ modifiedAt: number,
+): string => {
+ return md5(id + modifiedAt + filepath + (self._version as string));
+};
+
/**
* For resolve environment mismatch issue.
*
@@ -54,6 +79,7 @@ export const getIdByOptions = ({
*
* Override `NODE_ENV`, `BABEL_ENV` to bundler's environment.
*
+ * @param isDev - env is 'development'
* @see {@link https://github.com/babel/babel/blob/v7.23.0/packages/babel-core/src/config/helpers/environment.ts#L2}
*/
export const setEnvironment = (isDev: boolean): void => {
@@ -62,10 +88,6 @@ export const setEnvironment = (isDev: boolean): void => {
process.env.BABEL_ENV = env;
};
-export const ASSET_PATH = 'assets';
-export const PUBLIC_PATH = 'public';
-export const STATUS_CACHE_FILE = 'build-status.json';
-
export const getDevServerPublicPath = (root: string): string => {
return path.resolve(root, LOCAL_CACHE_DIR, PUBLIC_PATH);
};
diff --git a/packages/shared/lib/index.ts b/packages/shared/lib/index.ts
new file mode 100644
index 00000000..ea0144ca
--- /dev/null
+++ b/packages/shared/lib/index.ts
@@ -0,0 +1,18 @@
+import colors from 'colors';
+import { isCI, isTTY } from './utils';
+
+(isCI() || !isTTY()) && colors.disable();
+
+// Data, utilities, helpers
+export * as colors from 'colors';
+export * from './constants';
+export * from './helpers';
+export * from './misc';
+export * from './utils';
+export * from './enums';
+export { Logger, LogLevel } from './logger';
+
+// Shared types
+export type * from './types/config';
+export type * from './types/core';
+export type * from './types/transformers';
diff --git a/packages/utils/lib/logger.ts b/packages/shared/lib/logger.ts
similarity index 100%
rename from packages/utils/lib/logger.ts
rename to packages/shared/lib/logger.ts
diff --git a/packages/core/lib/bundler/logo.ts b/packages/shared/lib/misc.ts
similarity index 93%
rename from packages/core/lib/bundler/logo.ts
rename to packages/shared/lib/misc.ts
index 64e2cc26..73232238 100644
--- a/packages/core/lib/bundler/logo.ts
+++ b/packages/shared/lib/misc.ts
@@ -1,4 +1,5 @@
-import { colors, isTTY } from '@react-native-esbuild/utils';
+import * as colors from 'colors';
+import { isTTY } from './utils';
const LOGO = `
"88e "88e
@@ -12,9 +13,10 @@ const LOGO = `
// Center column index of `LOGO`
const LOGO_CENTER_X = 18;
-export const ESBUILD_LABEL = ' » esbuild ';
const DESCRIPTION = 'An extremely fast bundler';
+export const ESBUILD_LABEL = ' » esbuild ';
+
export const printLogo = (): void => {
if (isTTY()) {
process.stdout.write(`${colors.yellow(LOGO)}\n`);
diff --git a/packages/shared/lib/types/config.ts b/packages/shared/lib/types/config.ts
new file mode 100644
index 00000000..381109b8
--- /dev/null
+++ b/packages/shared/lib/types/config.ts
@@ -0,0 +1,129 @@
+import type { Plugin } from 'esbuild';
+import type { BabelTransformRule, SwcTransformRule } from './transformers';
+
+export interface Config {
+ /**
+ * Enable cache.
+ *
+ * Defaults to `true`
+ */
+ cache?: boolean;
+ /**
+ * Logger configurations
+ */
+ logger?: {
+ /**
+ * Disable log.
+ *
+ * Defaults to `false`
+ */
+ disabled?: boolean;
+ /**
+ * Print timestamp with log when format is specified.
+ *
+ * Defaults to `null`
+ */
+ timestamp?: string | null;
+ };
+ /**
+ * Resolver configurations
+ */
+ resolver?: {
+ /**
+ * Field names for resolve package's modules.
+ *
+ * Defaults to `['react-native', 'browser', 'main', 'module']`
+ */
+ mainFields?: string[];
+ /**
+ * File extensions for transform.
+ *
+ * Defaults: https://github.com/leegeunhyeok/react-native-esbuild/blob/master/packages/internal/lib/defaults.ts
+ */
+ sourceExtensions?: string[];
+ /**
+ * File extensions for assets registration.
+ *
+ * Defaults: https://github.com/leegeunhyeok/react-native-esbuild/blob/master/packages/internal/lib/defaults.ts
+ */
+ assetExtensions?: string[];
+ };
+ /**
+ * Transformer configurations
+ */
+ transformer?: {
+ /**
+ * Strip flow syntax.
+ *
+ * Defaults to `['react-native']`
+ */
+ stripFlowPackageNames?: string[];
+ /**
+ * Transform with babel using `metro-react-native-babel-preset` (slow)
+ */
+ fullyTransformPackageNames?: string[];
+ /**
+ * Additional transform rules. This rules will be applied before phase of transform to es5.
+ */
+ additionalTransformRules?: {
+ /**
+ * Additional babel transform rules
+ */
+ babel?: BabelTransformRule[];
+ /**
+ * Additional swc transform rules
+ */
+ swc?: SwcTransformRule[];
+ };
+ };
+ /**
+ * Web configurations
+ */
+ web?: {
+ /**
+ * Index page template file path
+ */
+ template?: string;
+ /**
+ * Placeholders for replacement
+ *
+ * ```js
+ * // web.placeholders
+ * { placeholder_name: 'Hello, world!' };
+ * ```
+ *
+ * will be replaced to
+ *
+ * ```html
+ *
+ * {{placeholder_name}}
+ *
+ *
+ * Hello, world!
+ * ```
+ *
+ * ---
+ *
+ * Reserved placeholder name
+ *
+ * - `root`: root tag id
+ * - `script`: bundled script path
+ */
+ placeholders?: Record;
+ };
+ /**
+ * Additional Esbuild plugins.
+ */
+ plugins?: Plugin[];
+ /**
+ * Experimental configurations
+ */
+ experimental?: {
+ /**
+ * Enable HMR(Hot Module Replacement) on development mode.
+ *
+ * Defaults to `false`.
+ */
+ hmr?: boolean;
+ };
+}
diff --git a/packages/shared/lib/types/core.ts b/packages/shared/lib/types/core.ts
new file mode 100644
index 00000000..0d6c5dd7
--- /dev/null
+++ b/packages/shared/lib/types/core.ts
@@ -0,0 +1,95 @@
+import type { BuildResult, Plugin } from 'esbuild';
+import type { BuildMode } from '../enums';
+import type { Config } from './config';
+
+export type SupportedPlatform = 'android' | 'ios' | 'web';
+
+// Internal task id.
+export type Id = number;
+
+// Internal bundler options.
+export interface BundleOptions {
+ platform: SupportedPlatform;
+ entry: string;
+ outfile: string;
+ dev: boolean;
+ minify: boolean;
+ metafile: boolean;
+ sourcemap?: string;
+ assetsDir?: string;
+}
+
+export interface BuildContext {
+ id: Id;
+ root: string;
+ mode: BuildMode;
+ config: Config;
+ bundleOptions: BundleOptions;
+ moduleManager: ModuleManager;
+ cacheStorage: CacheStorage;
+ hmrEnabled: boolean;
+ /**
+ * ```ts
+ * interface AdditionalData {
+ * // Set string value when HMR is enabled.
+ * externalPattern?: string;
+ * }
+ * ```
+ */
+ additionalData: AdditionalData;
+}
+
+export type AdditionalData = Record;
+
+export interface BuildStatusListener {
+ onBuildStart: (context: BuildContext) => void;
+ onBuild: (context: BuildContext, status: BuildStatus) => void;
+ onBuildEnd: (
+ context: BuildContext,
+ data: {
+ result: BuildResult;
+ success: boolean;
+ },
+ status: BuildStatus,
+ ) => void;
+}
+
+/**
+ * - `total`: total module count
+ * - `resolved`: resolved module count
+ * - `loaded`: loaded module count
+ */
+export interface BuildStatus {
+ total: number;
+ resolved: number;
+ loaded: number;
+}
+
+// Internal plugin factory.
+export type PluginFactory = (
+ context: BuildContext,
+ config?: PluginConfig,
+) => Plugin;
+
+// Caches
+export interface Cache {
+ data: string;
+ mtimeMs: number;
+}
+
+export interface CacheStorage {
+ readFromMemory: (key: string) => Cache | undefined;
+ readFromFileSystem: (key: string) => Promise;
+ writeToMemory: (key: string, cacheData: Cache) => void;
+ writeToFileSystem: (key: string, data: string) => Promise;
+}
+
+// Modules
+export type ModuleId = number;
+
+export interface ModuleManager {
+ /**
+ * @param modulePath - actual module path(`path` value in esbuild metafile).
+ */
+ getModuleId: (modulePath: string, isEntryPoint?: boolean) => number;
+}
diff --git a/packages/shared/lib/types/transformers.ts b/packages/shared/lib/types/transformers.ts
new file mode 100644
index 00000000..82f4ea37
--- /dev/null
+++ b/packages/shared/lib/types/transformers.ts
@@ -0,0 +1,16 @@
+import type { TransformOptions as BabelTransformOptions } from '@babel/core';
+import type { Options as SwcTransformOptions } from '@swc/core';
+
+export interface TransformRuleBase {
+ /**
+ * Predicator for transform
+ */
+ test: (path: string, code: string) => boolean;
+ /**
+ * Transformer options
+ */
+ options: T | ((path: string, code: string) => T);
+}
+
+export type BabelTransformRule = TransformRuleBase;
+export type SwcTransformRule = TransformRuleBase;
diff --git a/packages/utils/lib/env.ts b/packages/shared/lib/utils.ts
similarity index 100%
rename from packages/utils/lib/env.ts
rename to packages/shared/lib/utils.ts
diff --git a/packages/utils/package.json b/packages/shared/package.json
similarity index 82%
rename from packages/utils/package.json
rename to packages/shared/package.json
index a3092323..49e2f9a5 100644
--- a/packages/utils/package.json
+++ b/packages/shared/package.json
@@ -1,7 +1,7 @@
{
- "name": "@react-native-esbuild/utils",
+ "name": "@react-native-esbuild/shared",
"version": "0.1.0-beta.12",
- "description": "utilities for @react-native-esbuild",
+ "description": "shared data and utilities for @react-native-esbuild",
"keywords": [
"react-native",
"esbuild"
@@ -37,12 +37,16 @@
"url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
},
"devDependencies": {
+ "@babel/core": "^7.23.2",
"@faker-js/faker": "^8.1.0",
+ "@swc/core": "^1.3.95",
+ "@types/md5": "^2.3.5",
"esbuild": "^0.19.5"
},
"dependencies": {
"colors": "^1.4.0",
"dayjs": "^1.11.10",
+ "md5": "^2.3.0",
"node-self": "^1.0.2"
}
}
diff --git a/packages/config/tsconfig.json b/packages/shared/tsconfig.json
similarity index 88%
rename from packages/config/tsconfig.json
rename to packages/shared/tsconfig.json
index 83cc53a6..bd42e358 100644
--- a/packages/config/tsconfig.json
+++ b/packages/shared/tsconfig.json
@@ -3,6 +3,7 @@
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
+ "declaration": true,
"emitDeclarationOnly": true
},
"include": ["./lib"],
diff --git a/packages/symbolicate/lib/symbolicate.ts b/packages/symbolicate/lib/symbolicate.ts
index de99916c..45d6f9d1 100644
--- a/packages/symbolicate/lib/symbolicate.ts
+++ b/packages/symbolicate/lib/symbolicate.ts
@@ -65,7 +65,6 @@ const originalPositionFor = (
);
return {
...targetFrame,
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Allow for `Object.entries()`.
...(value ? { [targetKey]: value } : null),
};
},
@@ -101,7 +100,7 @@ const getCodeFrame = (
const source = sourcemapConsumer.sourceContentFor(frame.file);
const { lineNumber, column, file } = frame;
return {
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment -- `codeFrameColumns` type isn't defined.
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call -- `codeFrameColumns` type isn't defined.
content: codeFrameColumns(
source,
{
diff --git a/packages/transformer/README.md b/packages/transformer/README.md
index 2982f484..29ac1ee5 100644
--- a/packages/transformer/README.md
+++ b/packages/transformer/README.md
@@ -7,11 +7,9 @@ import {
stripFlowWithSucrase,
transformWithBabel,
transformWithSwc,
- minifyWithSwc,
} from '@react-native-esbuild/transformer';
await stripFlowWithSucrase(code, context);
await transformWithBabel(code, context, options);
await transformWithSwc(code, context, options);
-await minifyWithSwc(code, context, options);
```
diff --git a/packages/transformer/lib/common/builder.ts b/packages/transformer/lib/common/builder.ts
new file mode 100644
index 00000000..7fa6ddb0
--- /dev/null
+++ b/packages/transformer/lib/common/builder.ts
@@ -0,0 +1,35 @@
+import type { BuildContext } from '@react-native-esbuild/shared';
+import { getReactNativeInitializeCore } from '@react-native-esbuild/internal';
+import { AsyncTransformPipeline } from '../pipelines';
+import { swcPresets } from '../transformer';
+
+export const getCommonReactNativeRuntimePipelineBuilder = (
+ context: BuildContext,
+): InstanceType => {
+ const {
+ stripFlowPackageNames = [],
+ fullyTransformPackageNames = [],
+ additionalTransformRules,
+ } = context.config.transformer ?? {};
+ const additionalBabelRules = additionalTransformRules?.babel ?? [];
+ const additionalSwcRules = additionalTransformRules?.swc ?? [];
+ const injectScriptPaths = [
+ getReactNativeInitializeCore(context.root),
+ // `hmr/runtime` should import after `initializeCore` initialized.
+ context.hmrEnabled ? '@react-native-esbuild/hmr/runtime' : undefined,
+ ].filter(Boolean) as string[];
+
+ const builder = new AsyncTransformPipeline.builder({
+ dev: context.bundleOptions.dev,
+ entry: context.bundleOptions.entry,
+ root: context.root,
+ })
+ .setSwcPreset(swcPresets.getReactNativeRuntimePreset())
+ .setInjectScripts(injectScriptPaths)
+ .setFullyTransformPackages(fullyTransformPackageNames)
+ .setStripFlowPackages(stripFlowPackageNames)
+ .setAdditionalBabelTransformRules(additionalBabelRules)
+ .setAdditionalSwcTransformRules(additionalSwcRules);
+
+ return builder;
+};
diff --git a/packages/transformer/lib/common/index.ts b/packages/transformer/lib/common/index.ts
new file mode 100644
index 00000000..ecea700b
--- /dev/null
+++ b/packages/transformer/lib/common/index.ts
@@ -0,0 +1 @@
+export * from './builder';
diff --git a/packages/transformer/lib/helpers/transformer.ts b/packages/transformer/lib/helpers/transformer.ts
index 4e4877b3..04bda9e8 100644
--- a/packages/transformer/lib/helpers/transformer.ts
+++ b/packages/transformer/lib/helpers/transformer.ts
@@ -1,16 +1,15 @@
+import type {
+ BabelTransformRule,
+ SwcTransformRule,
+ TransformRuleBase,
+} from '@react-native-esbuild/shared';
import {
transformWithBabel,
transformSyncWithBabel,
transformWithSwc,
transformSyncWithSwc,
} from '../transformer';
-import type {
- TransformRuleBase,
- TransformerContext,
- SwcTransformRule,
- BabelTransformRule,
- TransformerOptionsPreset,
-} from '../types';
+import type { TransformContext, TransformerOptionsPreset } from '../types';
const ruleOptionsToPreset = (
options: TransformRuleBase['options'],
@@ -24,47 +23,51 @@ const ruleOptionsToPreset = (
export const transformByBabelRule = (
rule: BabelTransformRule,
code: string,
- context: TransformerContext,
+ context: TransformContext,
): Promise => {
return rule.test(context.path, code)
- ? transformWithBabel(code, context, ruleOptionsToPreset(rule.options, code))
+ ? transformWithBabel(code, {
+ context,
+ preset: ruleOptionsToPreset(rule.options, code),
+ })
: Promise.resolve(null);
};
export const transformSyncByBabelRule = (
rule: BabelTransformRule,
code: string,
- context: TransformerContext,
+ context: TransformContext,
): string | null => {
return rule.test(context.path, code)
- ? transformSyncWithBabel(
- code,
+ ? transformSyncWithBabel(code, {
context,
- ruleOptionsToPreset(rule.options, code),
- )
+ preset: ruleOptionsToPreset(rule.options, code),
+ })
: null;
};
export const transformBySwcRule = (
rule: SwcTransformRule,
code: string,
- context: TransformerContext,
+ context: TransformContext,
): Promise => {
return rule.test(context.path, code)
- ? transformWithSwc(code, context, ruleOptionsToPreset(rule.options, code))
+ ? transformWithSwc(code, {
+ context,
+ preset: ruleOptionsToPreset(rule.options, code),
+ })
: Promise.resolve(null);
};
export const transformSyncBySwcRule = (
rule: SwcTransformRule,
code: string,
- context: TransformerContext,
+ context: TransformContext,
): string | null => {
return rule.test(context.path, code)
- ? transformSyncWithSwc(
- code,
+ ? transformSyncWithSwc(code, {
context,
- ruleOptionsToPreset(rule.options, code),
- )
+ preset: ruleOptionsToPreset(rule.options, code),
+ })
: null;
};
diff --git a/packages/transformer/lib/index.ts b/packages/transformer/lib/index.ts
index eef47918..eca6e441 100644
--- a/packages/transformer/lib/index.ts
+++ b/packages/transformer/lib/index.ts
@@ -1,4 +1,5 @@
export * from './transformer';
export * from './pipelines';
export * from './helpers';
+export * from './common';
export type * from './types';
diff --git a/packages/transformer/lib/pipelines/AsyncTransformPipeline.ts b/packages/transformer/lib/pipelines/AsyncTransformPipeline.ts
deleted file mode 100644
index e3330a0c..00000000
--- a/packages/transformer/lib/pipelines/AsyncTransformPipeline.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import path from 'node:path';
-import fs from 'node:fs/promises';
-import type { OnLoadArgs } from 'esbuild';
-import {
- transformWithBabel,
- transformWithSwc,
- stripFlowWithSucrase,
-} from '../transformer';
-import { transformByBabelRule, transformBySwcRule } from '../helpers';
-import type { AsyncTransformStep, ModuleMeta, TransformResult } from '../types';
-import { TransformPipeline } from './pipeline';
-import { TransformPipelineBuilder } from './builder';
-
-export class AsyncTransformPipelineBuilder extends TransformPipelineBuilder<
- AsyncTransformStep,
- AsyncTransformPipeline
-> {
- build(): AsyncTransformPipeline {
- const pipeline = new AsyncTransformPipeline(this.context);
-
- this.onBefore && pipeline.beforeTransform(this.onBefore);
- this.onAfter && pipeline.afterTransform(this.onAfter);
-
- // 1. Inject initializeCore and specified scripts to entry file.
- if (this.injectScriptPaths.length) {
- const entryFile = path.resolve(this.context.root, this.context.entry);
- pipeline.addStep((code, args) => {
- return Promise.resolve({
- code:
- args.path === entryFile
- ? this.combineInjectScripts(code, this.injectScriptPaths)
- : code,
- done: false,
- });
- });
- }
-
- // 2. Fully transform and skip other steps.
- const fullyTransformPackagesRegExp = this.getNodePackageRegExp(
- this.fullyTransformPackageNames,
- );
- if (fullyTransformPackagesRegExp) {
- pipeline.addStep(async (code, args) => {
- if (fullyTransformPackagesRegExp.test(args.path)) {
- return {
- code: await transformWithBabel(
- code,
- this.getContext(args),
- this.presets.babelFullyTransform,
- ),
- // skip other transformations when fully transformed
- done: true,
- };
- }
- return { code, done: false };
- });
- }
-
- // 3. Strip flow syntax.
- const stripFlowPackageNamesRegExp = this.getNodePackageRegExp(
- this.stripFlowPackageNames,
- );
- if (stripFlowPackageNamesRegExp) {
- pipeline.addStep((code, args) => {
- if (
- stripFlowPackageNamesRegExp.test(args.path) ||
- this.isFlow(code, args.path)
- ) {
- code = stripFlowWithSucrase(code, this.getContext(args));
- }
-
- return Promise.resolve({ code, done: false });
- });
- }
-
- // 4. Apply additional babel rules.
- if (this.additionalBabelRules.length) {
- pipeline.addStep(async (code, args) => {
- const context = this.getContext(args);
- for await (const rule of this.additionalBabelRules) {
- code = (await transformByBabelRule(rule, code, context)) ?? code;
- }
- return { code, done: false };
- });
- }
-
- // 5. Apply additional swc rules.
- if (this.additionalSwcRules.length) {
- pipeline.addStep(async (code, args) => {
- const context = this.getContext(args);
- for await (const rule of this.additionalSwcRules) {
- code = (await transformBySwcRule(rule, code, context)) ?? code;
- }
- return { code, done: false };
- });
- }
-
- // 6. Transform code to es5.
- pipeline.addStep(async (code, args) => {
- return {
- code: await transformWithSwc(
- code,
- this.getContext(args),
- this.swcPreset,
- ),
- done: true,
- };
- });
-
- return pipeline;
- }
-}
-
-export class AsyncTransformPipeline extends TransformPipeline {
- public static builder = AsyncTransformPipelineBuilder;
- protected steps: AsyncTransformStep[] = [];
- protected onBeforeTransform?: AsyncTransformStep;
- protected onAfterTransform?: AsyncTransformStep;
-
- async transform(code: string, args: OnLoadArgs): Promise {
- const fileStat = await fs.stat(args.path);
- const moduleMeta: ModuleMeta = {
- stats: fileStat,
- hash: this.getHash(this.context.id, args.path, fileStat.mtimeMs),
- };
-
- const before: AsyncTransformStep = (code, args) => {
- return this.onBeforeTransform
- ? this.onBeforeTransform(code, args, moduleMeta)
- : Promise.resolve({ code, done: false });
- };
-
- const after: AsyncTransformStep = (code, args) => {
- return this.onAfterTransform
- ? this.onAfterTransform(code, args, moduleMeta)
- : Promise.resolve({ code, done: true });
- };
-
- const result = await this.steps.reduce(
- (prev, curr) => {
- return Promise.resolve(prev).then((prevResult) =>
- prevResult.done
- ? Promise.resolve({ code: prevResult.code, done: true })
- : curr(prevResult.code, args, moduleMeta),
- );
- },
- before(code, args, moduleMeta),
- );
-
- return after(result.code, args, moduleMeta);
- }
-}
diff --git a/packages/transformer/lib/pipelines/AsyncTransformPipeline/AsyncTransformPipeline.ts b/packages/transformer/lib/pipelines/AsyncTransformPipeline/AsyncTransformPipeline.ts
new file mode 100644
index 00000000..b0ab82a0
--- /dev/null
+++ b/packages/transformer/lib/pipelines/AsyncTransformPipeline/AsyncTransformPipeline.ts
@@ -0,0 +1,131 @@
+import {
+ transformWithBabel,
+ transformWithSwc,
+ stripFlowWithSucrase,
+} from '../../transformer';
+import { transformByBabelRule, transformBySwcRule } from '../../helpers';
+import type {
+ AsyncTransformStep,
+ BaseTransformContext,
+ TransformContext,
+ TransformResult,
+} from '../../types';
+import { TransformPipeline } from '../TransformPipeline';
+import { TransformPipelineBuilder } from '../TransformPipelineBuilder';
+
+export class AsyncTransformPipelineBuilder extends TransformPipelineBuilder<
+ AsyncTransformStep,
+ AsyncTransformPipeline
+> {
+ build(): AsyncTransformPipeline {
+ const pipeline = new AsyncTransformPipeline(this.baseContext);
+
+ this.beforeTransformStep && pipeline.addStep(this.beforeTransformStep);
+
+ // 1. Inject initializeCore and specified scripts to entry file.
+ if (this.injectScriptPaths.length) {
+ pipeline.addStep((code, context) => {
+ return Promise.resolve({
+ code: context.pluginData?.isEntryPoint
+ ? this.combineInjectScripts(code, this.injectScriptPaths)
+ : code,
+ done: false,
+ });
+ });
+ }
+
+ // 2. Fully transform and skip other steps.
+ const fullyTransformPackagesRegExp = this.getNodePackageRegExp(
+ this.fullyTransformPackageNames,
+ );
+ if (fullyTransformPackagesRegExp) {
+ pipeline.addStep(async (code, context) => {
+ if (fullyTransformPackagesRegExp.test(context.path)) {
+ return {
+ code: await transformWithBabel(code, {
+ context,
+ preset: this.presets.babelFullyTransform,
+ }),
+ // skip other transformations when fully transformed
+ done: true,
+ };
+ }
+ return { code, done: false };
+ });
+ }
+
+ // 3. Strip flow syntax.
+ const stripFlowPackageNamesRegExp = this.getNodePackageRegExp(
+ this.stripFlowPackageNames,
+ );
+ if (stripFlowPackageNamesRegExp) {
+ pipeline.addStep((code, context) => {
+ if (
+ stripFlowPackageNamesRegExp.test(context.path) ||
+ this.isFlow(code, context.path)
+ ) {
+ code = stripFlowWithSucrase(code, { context });
+ }
+
+ return Promise.resolve({ code, done: false });
+ });
+ }
+
+ // 4. Apply additional babel rules.
+ if (this.additionalBabelRules.length) {
+ pipeline.addStep(async (code, context) => {
+ for await (const rule of this.additionalBabelRules) {
+ code = (await transformByBabelRule(rule, code, context)) ?? code;
+ }
+ return { code, done: false };
+ });
+ }
+
+ // 5. Apply additional swc rules.
+ if (this.additionalSwcRules.length) {
+ pipeline.addStep(async (code, context) => {
+ for await (const rule of this.additionalSwcRules) {
+ code = (await transformBySwcRule(rule, code, context)) ?? code;
+ }
+ return { code, done: false };
+ });
+ }
+
+ // 6. Transform code to es5.
+ pipeline.addStep(async (code, context) => {
+ return {
+ code: await transformWithSwc(code, {
+ context,
+ preset: this.swcPreset,
+ }),
+ done: true,
+ };
+ });
+
+ this.afterTransformStep && pipeline.addStep(this.afterTransformStep);
+
+ return pipeline;
+ }
+}
+
+export class AsyncTransformPipeline extends TransformPipeline {
+ public static builder = AsyncTransformPipelineBuilder;
+ protected steps: AsyncTransformStep[] = [];
+
+ async transform(
+ code: string,
+ context: Omit,
+ ): Promise {
+ const transformContext = { ...this.baseContext, ...context };
+ const transformResult = await this.steps.reduce(
+ (prev, curr) => {
+ return prev.then((result) =>
+ result.done ? result : curr(result.code, transformContext),
+ );
+ },
+ Promise.resolve({ code, done: false } as TransformResult),
+ );
+
+ return transformResult;
+ }
+}
diff --git a/packages/transformer/lib/pipelines/AsyncTransformPipeline/index.ts b/packages/transformer/lib/pipelines/AsyncTransformPipeline/index.ts
new file mode 100644
index 00000000..2719ba0f
--- /dev/null
+++ b/packages/transformer/lib/pipelines/AsyncTransformPipeline/index.ts
@@ -0,0 +1 @@
+export * from './AsyncTransformPipeline';
diff --git a/packages/transformer/lib/pipelines/HMRTransformPipeline/HMRTransformPipeline.ts b/packages/transformer/lib/pipelines/HMRTransformPipeline/HMRTransformPipeline.ts
new file mode 100644
index 00000000..29675053
--- /dev/null
+++ b/packages/transformer/lib/pipelines/HMRTransformPipeline/HMRTransformPipeline.ts
@@ -0,0 +1,92 @@
+import fs from 'node:fs/promises';
+import { isExternal, type DependencyGraph } from 'esbuild-dependency-graph';
+import {
+ colors,
+ type ModuleId,
+ type BuildContext,
+} from '@react-native-esbuild/shared';
+import {
+ asHMRUpdateCall,
+ asFallbackBoundary,
+ type HMRTransformResult,
+} from '@react-native-esbuild/hmr';
+import type { AsyncTransformPipeline } from '../AsyncTransformPipeline';
+import { getCommonReactNativeRuntimePipelineBuilder } from '../../common';
+import { logger } from '../../shared';
+
+export class HMRTransformPipeline {
+ private pipeline: AsyncTransformPipeline;
+ private dependencyGraph: DependencyGraph | null = null;
+
+ constructor(private buildContext: BuildContext) {
+ this.pipeline =
+ getCommonReactNativeRuntimePipelineBuilder(buildContext).build();
+ }
+
+ public setDependencyGraph(dependencyGraph: DependencyGraph): void {
+ this.dependencyGraph = dependencyGraph;
+ }
+
+ public async transformDelta(id: ModuleId): Promise {
+ try {
+ performance.mark(`hmr:build:${id}`);
+ const transformResult = await this.transform(id);
+ const { code, dependencies } = transformResult;
+ const { duration } = performance.measure(
+ 'hmr:build-duration',
+ `hmr:build:${id}`,
+ );
+ logger.info(
+ [
+ colors.gray(`+ ${dependencies} module(s)`),
+ 'transformed in',
+ colors.cyan(`${Math.floor(duration)}ms`),
+ ].join(' '),
+ );
+ return { id, code, fullyReload: false };
+ } catch (error) {
+ logger.error('unable to transform runtime modules', error as Error);
+ return { id, code: '', fullyReload: true };
+ }
+ }
+
+ private async transform(
+ id: ModuleId,
+ ): Promise<{ code: string; dependencies: number }> {
+ const dependencyGraph = this.dependencyGraph;
+ if (!dependencyGraph) {
+ throw new Error('dependency graph is not initialized');
+ }
+
+ const inverseDependencies = dependencyGraph.inverseDependenciesOf(id);
+ const transformedCodes = await Promise.all(
+ [id, ...inverseDependencies].map(async (moduleId) => {
+ const module = dependencyGraph.getModule(moduleId);
+
+ if (isExternal(module)) {
+ logger.debug('external module found', { path: module.path });
+ return '';
+ }
+
+ const rawCode = await fs.readFile(module.path, 'utf-8');
+ logger.debug(`${colors.cyan(module.path)} imports`);
+
+ const { code } = await this.pipeline.transform(rawCode, {
+ id: moduleId,
+ path: module.path,
+ pluginData: {
+ isEntryPoint: false,
+ externalPattern: this.buildContext.additionalData.externalPattern,
+ },
+ });
+
+ return asHMRUpdateCall(id, code);
+ }),
+ );
+
+ return {
+ code: asFallbackBoundary(transformedCodes.join('\n')),
+ dependencies: inverseDependencies.length - 1,
+ };
+ }
+}
diff --git a/packages/transformer/lib/pipelines/HMRTransformPipeline/index.ts b/packages/transformer/lib/pipelines/HMRTransformPipeline/index.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/transformer/lib/pipelines/SyncTransformPipeline.ts b/packages/transformer/lib/pipelines/SyncTransformPipeline.ts
deleted file mode 100644
index 8eef2f51..00000000
--- a/packages/transformer/lib/pipelines/SyncTransformPipeline.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import path from 'node:path';
-import fs from 'node:fs';
-import type { OnLoadArgs } from 'esbuild';
-import {
- transformSyncWithBabel,
- transformSyncWithSwc,
- stripFlowWithSucrase,
-} from '../transformer';
-import { transformSyncByBabelRule, transformSyncBySwcRule } from '../helpers';
-import type { SyncTransformStep, TransformResult, ModuleMeta } from '../types';
-import { TransformPipeline } from './pipeline';
-import { TransformPipelineBuilder } from './builder';
-
-export class SyncTransformPipelineBuilder extends TransformPipelineBuilder<
- SyncTransformStep,
- SyncTransformPipeline
-> {
- build(): SyncTransformPipeline {
- const pipeline = new SyncTransformPipeline(this.context);
-
- this.onBefore && pipeline.beforeTransform(this.onBefore);
- this.onAfter && pipeline.afterTransform(this.onAfter);
-
- // 1. Inject initializeCore and specified scripts to entry file.
- if (this.injectScriptPaths.length) {
- const entryFile = path.resolve(this.context.root, this.context.entry);
- pipeline.addStep((code, args) => {
- return {
- code:
- args.path === entryFile
- ? this.combineInjectScripts(code, this.injectScriptPaths)
- : code,
- done: false,
- };
- });
- }
-
- // 2. Fully transform and skip other steps.
- const fullyTransformPackagesRegExp = this.getNodePackageRegExp(
- this.fullyTransformPackageNames,
- );
- if (fullyTransformPackagesRegExp) {
- pipeline.addStep((code, args) => {
- if (fullyTransformPackagesRegExp.test(args.path)) {
- return {
- code: transformSyncWithBabel(
- code,
- this.getContext(args),
- this.presets.babelFullyTransform,
- ),
- // skip other transformations when fully transformed
- done: true,
- };
- }
- return { code, done: false };
- });
- }
-
- // 3. Strip flow syntax.
- const stripFlowPackageNamesRegExp = this.getNodePackageRegExp(
- this.stripFlowPackageNames,
- );
- if (stripFlowPackageNamesRegExp) {
- pipeline.addStep((code, args) => {
- if (
- stripFlowPackageNamesRegExp.test(args.path) ||
- this.isFlow(code, args.path)
- ) {
- code = stripFlowWithSucrase(code, this.getContext(args));
- }
-
- return { code, done: false };
- });
- }
-
- // 4. Apply additional babel rules.
- if (this.additionalBabelRules.length) {
- pipeline.addStep((code, args) => {
- const context = this.getContext(args);
- for (const rule of this.additionalBabelRules) {
- code = transformSyncByBabelRule(rule, code, context) ?? code;
- }
- return { code, done: false };
- });
- }
-
- // 5. Apply additional swc rules.
- if (this.additionalSwcRules.length) {
- pipeline.addStep((code, args) => {
- const context = this.getContext(args);
- for (const rule of this.additionalSwcRules) {
- code = transformSyncBySwcRule(rule, code, context) ?? code;
- }
- return { code, done: false };
- });
- }
-
- // 6. Transform code to es5.
- pipeline.addStep((code, args) => {
- return {
- code: transformSyncWithSwc(code, this.getContext(args), this.swcPreset),
- done: true,
- };
- });
-
- return pipeline;
- }
-}
-
-export class SyncTransformPipeline extends TransformPipeline {
- public static builder = SyncTransformPipelineBuilder;
- protected steps: SyncTransformStep[] = [];
- protected onBeforeTransform?: SyncTransformStep;
- protected onAfterTransform?: SyncTransformStep;
-
- beforeTransform(onBeforeTransform: SyncTransformStep): this {
- this.onBeforeTransform = onBeforeTransform;
- return this;
- }
-
- afterTransform(onAfterTransform: SyncTransformStep): this {
- this.onAfterTransform = onAfterTransform;
- return this;
- }
-
- addStep(runner: SyncTransformStep): this {
- this.steps.push(runner);
- return this;
- }
-
- transform(code: string, args: OnLoadArgs): TransformResult {
- const fileStat = fs.statSync(args.path);
- const moduleMeta: ModuleMeta = {
- stats: fileStat,
- hash: this.getHash(this.context.id, args.path, fileStat.mtimeMs),
- };
-
- const before: SyncTransformStep = (code, args) => {
- return this.onBeforeTransform
- ? this.onBeforeTransform(code, args, moduleMeta)
- : { code, done: false };
- };
-
- const after: SyncTransformStep = (code, args) => {
- return this.onAfterTransform
- ? this.onAfterTransform(code, args, moduleMeta)
- : { code, done: true };
- };
-
- const result = this.steps.reduce(
- (prev, curr) => {
- return prev.done
- ? { code: prev.code, done: true }
- : curr(prev.code, args, moduleMeta);
- },
- before(code, args, moduleMeta),
- );
-
- return after(result.code, args, moduleMeta);
- }
-}
diff --git a/packages/transformer/lib/pipelines/SyncTransformPipeline/SyncTransformPipeline.ts b/packages/transformer/lib/pipelines/SyncTransformPipeline/SyncTransformPipeline.ts
new file mode 100644
index 00000000..1b9a76be
--- /dev/null
+++ b/packages/transformer/lib/pipelines/SyncTransformPipeline/SyncTransformPipeline.ts
@@ -0,0 +1,135 @@
+import {
+ transformSyncWithBabel,
+ transformSyncWithSwc,
+ stripFlowWithSucrase,
+} from '../../transformer';
+import {
+ transformSyncByBabelRule,
+ transformSyncBySwcRule,
+} from '../../helpers';
+import type {
+ SyncTransformStep,
+ TransformResult,
+ TransformContext,
+ BaseTransformContext,
+} from '../../types';
+import { TransformPipeline } from '../TransformPipeline';
+import { TransformPipelineBuilder } from '../TransformPipelineBuilder';
+
+export class SyncTransformPipelineBuilder extends TransformPipelineBuilder<
+ SyncTransformStep,
+ SyncTransformPipeline
+> {
+ build(): SyncTransformPipeline {
+ const pipeline = new SyncTransformPipeline(this.baseContext);
+
+ this.beforeTransformStep && pipeline.addStep(this.beforeTransformStep);
+
+ // 1. Inject initializeCore and specified scripts to entry file.
+ if (this.injectScriptPaths.length) {
+ pipeline.addStep((code, context) => {
+ return {
+ code: context.pluginData?.isEntryPoint
+ ? this.combineInjectScripts(code, this.injectScriptPaths)
+ : code,
+ done: false,
+ };
+ });
+ }
+
+ // 2. Fully transform and skip other steps.
+ const fullyTransformPackagesRegExp = this.getNodePackageRegExp(
+ this.fullyTransformPackageNames,
+ );
+ if (fullyTransformPackagesRegExp) {
+ pipeline.addStep((code, context) => {
+ if (fullyTransformPackagesRegExp.test(context.path)) {
+ return {
+ code: transformSyncWithBabel(code, {
+ context,
+ preset: this.presets.babelFullyTransform,
+ }),
+ // skip other transformations when fully transformed
+ done: true,
+ };
+ }
+ return { code, done: false };
+ });
+ }
+
+ // 3. Strip flow syntax.
+ const stripFlowPackageNamesRegExp = this.getNodePackageRegExp(
+ this.stripFlowPackageNames,
+ );
+ if (stripFlowPackageNamesRegExp) {
+ pipeline.addStep((code, context) => {
+ if (
+ stripFlowPackageNamesRegExp.test(context.path) ||
+ this.isFlow(code, context.path)
+ ) {
+ code = stripFlowWithSucrase(code, { context });
+ }
+
+ return { code, done: false };
+ });
+ }
+
+ // 4. Apply additional babel rules.
+ if (this.additionalBabelRules.length) {
+ pipeline.addStep((code, context) => {
+ for (const rule of this.additionalBabelRules) {
+ code = transformSyncByBabelRule(rule, code, context) ?? code;
+ }
+ return { code, done: false };
+ });
+ }
+
+ // 5. Apply additional swc rules.
+ if (this.additionalSwcRules.length) {
+ pipeline.addStep((code, context) => {
+ for (const rule of this.additionalSwcRules) {
+ code = transformSyncBySwcRule(rule, code, context) ?? code;
+ }
+ return { code, done: false };
+ });
+ }
+
+ // 6. Transform code to es5.
+ pipeline.addStep((code, context) => {
+ return {
+ code: transformSyncWithSwc(code, {
+ context,
+ preset: this.swcPreset,
+ }),
+ done: true,
+ };
+ });
+
+ this.afterTransformStep && pipeline.addStep(this.afterTransformStep);
+
+ return pipeline;
+ }
+}
+
+export class SyncTransformPipeline extends TransformPipeline {
+ public static builder = SyncTransformPipelineBuilder;
+ protected steps: SyncTransformStep[] = [];
+
+ addStep(runner: SyncTransformStep): this {
+ this.steps.push(runner);
+ return this;
+ }
+
+ transform(
+ code: string,
+ context: Omit,
+ ): TransformResult {
+ const transformContext = { ...this.baseContext, ...context };
+ const transformResult = this.steps.reduce(
+ (prev, curr) => (prev.done ? prev : curr(prev.code, transformContext)),
+ { code, done: false } as TransformResult,
+ );
+
+ return transformResult;
+ }
+}
diff --git a/packages/transformer/lib/pipelines/SyncTransformPipeline/index.ts b/packages/transformer/lib/pipelines/SyncTransformPipeline/index.ts
new file mode 100644
index 00000000..bf66b0eb
--- /dev/null
+++ b/packages/transformer/lib/pipelines/SyncTransformPipeline/index.ts
@@ -0,0 +1 @@
+export * from './SyncTransformPipeline';
diff --git a/packages/transformer/lib/pipelines/TransformPipeline.ts b/packages/transformer/lib/pipelines/TransformPipeline.ts
new file mode 100644
index 00000000..cc8c47ba
--- /dev/null
+++ b/packages/transformer/lib/pipelines/TransformPipeline.ts
@@ -0,0 +1,21 @@
+import type {
+ BaseTransformContext,
+ TransformStep,
+ TransformContext,
+} from '../types';
+
+export abstract class TransformPipeline> {
+ protected steps: Step[] = [];
+
+ constructor(protected baseContext: BaseTransformContext) {}
+
+ public addStep(step: Step): this {
+ this.steps.push(step);
+ return this;
+ }
+
+ abstract transform(
+ code: string,
+ context: Omit,
+ ): ReturnType;
+}
diff --git a/packages/transformer/lib/pipelines/builder.ts b/packages/transformer/lib/pipelines/TransformPipelineBuilder.ts
similarity index 82%
rename from packages/transformer/lib/pipelines/builder.ts
rename to packages/transformer/lib/pipelines/TransformPipelineBuilder.ts
index 366f9e28..d4243e8a 100644
--- a/packages/transformer/lib/pipelines/builder.ts
+++ b/packages/transformer/lib/pipelines/TransformPipelineBuilder.ts
@@ -1,14 +1,15 @@
-import type { OnLoadArgs } from 'esbuild';
import type { Options as SwcTransformOptions } from '@swc/core';
import type {
BabelTransformRule,
SwcTransformRule,
+} from '@react-native-esbuild/shared';
+import type {
+ BaseTransformContext,
TransformStep,
- TransformerContext,
TransformerOptionsPreset,
} from '../types';
import { babelPresets } from '../transformer';
-import type { TransformPipeline } from './pipeline';
+import type { TransformPipeline } from './TransformPipeline';
const FLOW_SYMBOL = ['@flow', '@noflow'] as const;
@@ -19,8 +20,8 @@ export abstract class TransformPipelineBuilder<
protected presets = {
babelFullyTransform: babelPresets.getFullyTransformPreset(),
};
- protected onBefore?: Step;
- protected onAfter?: Step;
+ protected beforeTransformStep?: Step;
+ protected afterTransformStep?: Step;
protected swcPreset?: TransformerOptionsPreset;
protected injectScriptPaths: string[] = [];
protected fullyTransformPackageNames: string[] = [];
@@ -28,7 +29,7 @@ export abstract class TransformPipelineBuilder<
protected additionalBabelRules: BabelTransformRule[] = [];
protected additionalSwcRules: SwcTransformRule[] = [];
- constructor(protected context: Omit) {}
+ constructor(protected baseContext: BaseTransformContext) {}
protected getNodePackageRegExp(packageNames: string[]): RegExp | null {
return packageNames.length
@@ -36,10 +37,6 @@ export abstract class TransformPipelineBuilder<
: null;
}
- protected getContext(args: OnLoadArgs): TransformerContext {
- return { ...this.context, path: args.path };
- }
-
protected combineInjectScripts(
code: string,
injectScriptPaths: string[],
@@ -57,13 +54,13 @@ export abstract class TransformPipelineBuilder<
);
}
- public onStart(transformer: Step): this {
- this.onBefore = transformer;
+ public beforeTransform(transformStep: Step): this {
+ this.beforeTransformStep = transformStep;
return this;
}
- public onEnd(transformer: Step): this {
- this.onAfter = transformer;
+ public afterTransform(transformStep: Step): this {
+ this.afterTransformStep = transformStep;
return this;
}
diff --git a/packages/transformer/lib/pipelines/pipeline.ts b/packages/transformer/lib/pipelines/pipeline.ts
deleted file mode 100644
index 84c84c3e..00000000
--- a/packages/transformer/lib/pipelines/pipeline.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { OnLoadArgs } from 'esbuild';
-import md5 from 'md5';
-import type { TransformStep, TransformerContext } from '../types';
-
-export abstract class TransformPipeline> {
- protected steps: Step[] = [];
- protected onBeforeTransform?: Step;
- protected onAfterTransform?: Step;
-
- constructor(protected context: Omit) {}
-
- /**
- * Generate hash that contains the file path, modification time, and bundle options.
- *
- * `id` is combined(platform, dev, minify) bundle options value in `@react-native-esbuild`.
- *
- * hash = md5(id + modified time + file path + core version)
- * number + number + string + string
- */
- protected getHash(id: number, filepath: string, modifiedAt: number): string {
- return md5(id + modifiedAt + filepath + self._version);
- }
-
- public beforeTransform(onBeforeTransform: Step): this {
- this.onBeforeTransform = onBeforeTransform;
- return this;
- }
-
- public afterTransform(onAfterTransform: Step): this {
- this.onAfterTransform = onAfterTransform;
- return this;
- }
-
- public addStep(step: Step): this {
- this.steps.push(step);
- return this;
- }
-
- abstract transform(code: string, args: OnLoadArgs): ReturnType;
-}
diff --git a/packages/transformer/lib/shared.ts b/packages/transformer/lib/shared.ts
new file mode 100644
index 00000000..d3a96fbb
--- /dev/null
+++ b/packages/transformer/lib/shared.ts
@@ -0,0 +1,3 @@
+import { Logger } from '@react-native-esbuild/shared';
+
+export const logger = new Logger('transformer');
diff --git a/packages/transformer/lib/transformer/babel/babel.ts b/packages/transformer/lib/transformer/babel/babel.ts
index 95261d9c..0a5fba78 100644
--- a/packages/transformer/lib/transformer/babel/babel.ts
+++ b/packages/transformer/lib/transformer/babel/babel.ts
@@ -3,11 +3,11 @@ import { loadOptions, transformAsync, transformSync } from '@babel/core';
import type {
AsyncTransformer,
SyncTransformer,
- TransformerContext,
+ TransformContext,
} from '../../types';
const loadBabelOptions = (
- context: TransformerContext,
+ context: TransformContext,
options?: TransformOptions,
): ReturnType => {
return loadOptions({
@@ -19,8 +19,7 @@ const loadBabelOptions = (
export const transformWithBabel: AsyncTransformer = async (
code: string,
- context,
- preset,
+ { context, preset },
) => {
const babelOptions = loadBabelOptions(context, preset?.(context));
if (!babelOptions) {
@@ -37,8 +36,7 @@ export const transformWithBabel: AsyncTransformer = async (
export const transformSyncWithBabel: SyncTransformer = (
code: string,
- context,
- preset,
+ { context, preset },
) => {
const babelOptions = loadBabelOptions(context, preset?.(context));
if (!babelOptions) {
diff --git a/packages/transformer/lib/transformer/sucrase/sucrase.ts b/packages/transformer/lib/transformer/sucrase/sucrase.ts
index a0d5baf2..5fc752ee 100644
--- a/packages/transformer/lib/transformer/sucrase/sucrase.ts
+++ b/packages/transformer/lib/transformer/sucrase/sucrase.ts
@@ -10,7 +10,10 @@ const stripFlowTypeofImportStatements = (code: string): string => {
.join('\n');
};
-export const stripFlowWithSucrase: SyncTransformer = (code, context) => {
+export const stripFlowWithSucrase: SyncTransformer = (
+ code,
+ { context },
+) => {
return stripFlowTypeofImportStatements(
transform(code, {
transforms: TRANSFORM_FOR_STRIP_FLOW,
diff --git a/packages/transformer/lib/transformer/swc/presets.ts b/packages/transformer/lib/transformer/swc/presets.ts
index 62eb43f2..e5a432bb 100644
--- a/packages/transformer/lib/transformer/swc/presets.ts
+++ b/packages/transformer/lib/transformer/swc/presets.ts
@@ -1,12 +1,13 @@
-import type {
- Options,
- JscConfig,
- TsParserConfig,
- EsParserConfig,
-} from '@swc/core';
+import type { Options, TsParserConfig, EsParserConfig } from '@swc/core';
+import {
+ REACT_REFRESH_GET_SIGNATURE_FUNCTION,
+ REACT_REFRESH_REGISTER_FUNCTION,
+ isHMRBoundary,
+} from '@react-native-esbuild/hmr';
import type {
TransformerOptionsPreset,
SwcJestPresetOptions,
+ SwcMinifyPresetOptions,
} from '../../types';
const getParserOptions = (path: string): TsParserConfig | EsParserConfig => {
@@ -26,27 +27,51 @@ const getParserOptions = (path: string): TsParserConfig | EsParserConfig => {
/**
* swc transform options preset for react-native runtime.
*/
-const getReactNativeRuntimePreset = (
- jscConfig?: Pick,
-): TransformerOptionsPreset => {
- return (context) => ({
- minify: false,
- sourceMaps: false,
- isModule: true,
- inputSourceMap: false,
- inlineSourcesContent: false,
- jsc: {
- parser: getParserOptions(context.path),
- target: 'es5',
- loose: false,
- externalHelpers: true,
- keepClassNames: true,
- transform: jscConfig?.transform,
- experimental: jscConfig?.experimental,
- },
- filename: context.path,
- root: context.root,
- });
+const getReactNativeRuntimePreset = (): TransformerOptionsPreset => {
+ return (context) => {
+ const hmrEnabled = isHMRBoundary(context.path);
+ return {
+ minify: false,
+ sourceMaps: false,
+ isModule: true,
+ inputSourceMap: false,
+ inlineSourcesContent: false,
+ jsc: {
+ parser: getParserOptions(context.path),
+ target: 'es5',
+ loose: false,
+ externalHelpers: !context.dev,
+ keepClassNames: true,
+ transform: {
+ react: {
+ development: context.dev,
+ refresh: hmrEnabled
+ ? {
+ refreshReg: REACT_REFRESH_REGISTER_FUNCTION,
+ refreshSig: REACT_REFRESH_GET_SIGNATURE_FUNCTION,
+ }
+ : undefined,
+ },
+ },
+ experimental: {
+ plugins: hmrEnabled
+ ? [
+ [
+ 'swc-plugin-global-module',
+ {
+ runtimeModule: context.pluginData?.isRuntimeModule,
+ externalPattern: context.pluginData?.externalPattern,
+ importPaths: context.pluginData?.importPaths,
+ },
+ ],
+ ]
+ : undefined,
+ },
+ },
+ filename: context.path,
+ root: context.root,
+ } as Options;
+ };
};
const getJestPreset = (
@@ -95,11 +120,21 @@ const getJestPreset = (
});
};
-const getMinifyPreset = () => {
- return () => ({
- compress: true,
- mangle: true,
- sourceMap: false,
+const getMinifyPreset = ({
+ minify,
+}: SwcMinifyPresetOptions): TransformerOptionsPreset => {
+ return (context) => ({
+ minify,
+ inputSourceMap: false,
+ inlineSourcesContent: false,
+ jsc: {
+ parser: getParserOptions(context.path),
+ target: 'es5',
+ loose: false,
+ keepClassNames: true,
+ },
+ filename: context.path,
+ root: context.root,
});
};
diff --git a/packages/transformer/lib/transformer/swc/swc.ts b/packages/transformer/lib/transformer/swc/swc.ts
index 9fe79f90..59b12b85 100644
--- a/packages/transformer/lib/transformer/swc/swc.ts
+++ b/packages/transformer/lib/transformer/swc/swc.ts
@@ -1,16 +1,9 @@
-import {
- transform,
- transformSync,
- minify,
- type Options,
- type JsMinifyOptions,
-} from '@swc/core';
+import { transform, transformSync, type Options } from '@swc/core';
import type { AsyncTransformer, SyncTransformer } from '../../types';
export const transformWithSwc: AsyncTransformer = async (
code,
- context,
- preset,
+ { context, preset },
) => {
const { code: transformedCode } = await transform(code, preset?.(context));
@@ -23,8 +16,7 @@ export const transformWithSwc: AsyncTransformer = async (
export const transformSyncWithSwc: SyncTransformer = (
code,
- context,
- preset,
+ { context, preset },
) => {
const { code: transformedCode } = transformSync(code, preset?.(context));
@@ -34,17 +26,3 @@ export const transformSyncWithSwc: SyncTransformer = (
return transformedCode;
};
-
-export const minifyWithSwc: AsyncTransformer = async (
- code,
- context,
- preset,
-) => {
- const { code: minifiedCode } = await minify(code, preset?.(context));
-
- if (typeof minifiedCode !== 'string') {
- throw new Error('swc minified source is empty');
- }
-
- return minifiedCode;
-};
diff --git a/packages/transformer/lib/types.ts b/packages/transformer/lib/types.ts
index 52591bbe..67b1642f 100644
--- a/packages/transformer/lib/types.ts
+++ b/packages/transformer/lib/types.ts
@@ -1,37 +1,58 @@
-import type { Stats } from 'node:fs';
import type { OnLoadArgs } from 'esbuild';
-import type { TransformOptions as BabelTransformOptions } from '@babel/core';
-import type { Options as SwcTransformOptions } from '@swc/core';
+import type { ModuleId } from '@react-native-esbuild/shared';
export type AsyncTransformer = (
code: string,
- context: TransformerContext,
- preset?: TransformerOptionsPreset,
+ config: TransformerConfig,
) => Promise;
export type SyncTransformer = (
code: string,
- context: TransformerContext,
- preset?: TransformerOptionsPreset,
+ config: TransformerConfig,
) => string;
-export interface TransformerContext {
- id: number;
+interface TransformerConfig {
+ context: TransformContext;
+ preset?: TransformerOptionsPreset;
+}
+
+export interface BaseTransformContext {
root: string;
entry: string;
dev: boolean;
+}
+
+export interface TransformContext extends BaseTransformContext {
+ id: ModuleId;
path: string;
+ /**
+ * @internal
+ *
+ * - `react-native-runtime-transform-plugin`
+ * - `mtimeMs`, `hash`, `externalPattern`
+ * - `HMRTransformPipeline`
+ * - `externalPattern`
+ *
+ * ```ts
+ * interface PluginData {
+ * // Set modified at timestamp.
+ * mtimeMs?: number;
+ * // Set hash value when cache enabled.
+ * hash?: string;
+ * // Set map value when HMR enabled.
+ * importPaths?: Record;
+ * // Set `true` if it's entry-point module.
+ * isEntryPoint?: boolean;
+ * }
+ * ```
+ */
+ pluginData: OnLoadArgs['pluginData'];
}
export type TransformerOptionsPreset = (
- context: TransformerContext,
+ context: TransformContext,
) => TransformerOptions;
-// swc preset options
-export interface SwcReactNativeRuntimePresetOptions {
- reactRefresh?: { moduleId: string };
-}
-
export interface SwcJestPresetOptions {
module?: 'cjs' | 'esm';
experimental?: {
@@ -63,25 +84,14 @@ export interface SwcJestPresetOptions {
};
}
-export interface TransformRuleBase {
- /**
- * Predicator for transform
- */
- test: (path: string, code: string) => boolean;
- /**
- * Transformer options
- */
- options: T | ((path: string, code: string) => T);
+export interface SwcMinifyPresetOptions {
+ minify: boolean;
}
-export type BabelTransformRule = TransformRuleBase;
-export type SwcTransformRule = TransformRuleBase;
-
// TransformPipelineBuilder
export type TransformStep = (
code: string,
- args: OnLoadArgs,
- moduleMeta: ModuleMeta,
+ context: TransformContext,
) => Result;
export type AsyncTransformStep = TransformStep>;
@@ -91,8 +101,3 @@ export interface TransformResult {
code: string;
done: boolean;
}
-
-export interface ModuleMeta {
- hash: string;
- stats: Stats;
-}
diff --git a/packages/transformer/package.json b/packages/transformer/package.json
index 4c10fcb3..6f3e920b 100644
--- a/packages/transformer/package.json
+++ b/packages/transformer/package.json
@@ -38,17 +38,19 @@
"url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
},
"devDependencies": {
- "@types/md5": "^2.3.4",
"esbuild": "^0.19.5"
},
"dependencies": {
"@babel/core": "^7.23.2",
- "@react-native-esbuild/config": "workspace:*",
+ "@react-native-esbuild/hmr": "workspace:*",
+ "@react-native-esbuild/internal": "workspace:^",
+ "@react-native-esbuild/shared": "workspace:*",
"@swc/core": "^1.3.95",
"@swc/helpers": "^0.5.3",
- "md5": "^2.3.0",
+ "esbuild-dependency-graph": "^0.3.0",
"sucrase": "^3.34.0",
"swc-plugin-coverage-instrument": "^0.0.20",
+ "swc-plugin-global-module": "^0.1.0-alpha.5",
"swc_mut_cjs_exports": "^0.85.0"
}
}
diff --git a/packages/utils/build/index.js b/packages/utils/build/index.js
deleted file mode 100644
index 11d3d095..00000000
--- a/packages/utils/build/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const esbuild = require('esbuild');
-const { getEsbuildBaseOptions } = require('../../../shared');
-
-const buildOptions = getEsbuildBaseOptions(__dirname);
-
-esbuild.build(buildOptions).catch((error) => {
- console.error(error);
- process.exit(1);
-});
diff --git a/packages/utils/lib/index.ts b/packages/utils/lib/index.ts
deleted file mode 100644
index 64ae3a2a..00000000
--- a/packages/utils/lib/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import colors from 'colors';
-import { isCI, isTTY } from './env';
-
-(isCI() || !isTTY()) && colors.disable();
-
-export * as colors from 'colors';
-export * from './env';
-export { Logger, LogLevel } from './logger';
diff --git a/shared/index.js b/shared/index.js
index 931f6fc5..33c3386d 100644
--- a/shared/index.js
+++ b/shared/index.js
@@ -1,15 +1,24 @@
const path = require('node:path');
/**
- * @param {string} entryFile filename
+ * @param {string} packageDir build script directory
* @param {import('esbuild').BuildOptions} options additional options
* @returns {import('esbuild').BuildOptions}
*/
-exports.getEsbuildBaseOptions = (packageDir, options = {}) => ({
- entryPoints: [path.resolve(packageDir, '../lib/index.ts')],
+const getEsbuildBaseOptions = (packageDir, options = {}) => ({
+ entryPoints: [path.join(getPackageRoot(packageDir), 'lib/index.ts')],
outfile: 'dist/index.js',
bundle: true,
platform: 'node',
packages: 'external',
...options,
});
+
+/**
+ * @param {string} packageDir build script directory
+ * @returns package root path
+ */
+const getPackageRoot = (packageDir) => path.resolve(packageDir, '../');
+
+exports.getEsbuildBaseOptions = getEsbuildBaseOptions;
+exports.getPackageRoot = getPackageRoot;
diff --git a/yarn.lock b/yarn.lock
index 42602e43..66a45d0a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3621,11 +3621,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "@react-native-esbuild/cli@workspace:packages/cli"
dependencies:
- "@react-native-esbuild/config": "workspace:*"
"@react-native-esbuild/core": "workspace:*"
"@react-native-esbuild/dev-server": "workspace:*"
"@react-native-esbuild/plugins": "workspace:*"
- "@react-native-esbuild/utils": "workspace:*"
+ "@react-native-esbuild/shared": "workspace:*"
"@types/yargs": ^17.0.24
esbuild: ^0.19.5
yargs: ^17.7.2
@@ -3635,28 +3634,22 @@ __metadata:
languageName: unknown
linkType: soft
-"@react-native-esbuild/config@workspace:*, @react-native-esbuild/config@workspace:packages/config":
- version: 0.0.0-use.local
- resolution: "@react-native-esbuild/config@workspace:packages/config"
- dependencies:
- esbuild: ^0.19.5
- languageName: unknown
- linkType: soft
-
"@react-native-esbuild/core@workspace:*, @react-native-esbuild/core@workspace:packages/core":
version: 0.0.0-use.local
resolution: "@react-native-esbuild/core@workspace:packages/core"
dependencies:
"@babel/core": ^7.23.2
"@faker-js/faker": ^8.1.0
- "@react-native-esbuild/config": "workspace:*"
+ "@react-native-esbuild/hmr": "workspace:*"
"@react-native-esbuild/internal": "workspace:*"
+ "@react-native-esbuild/plugins": "workspace:*"
+ "@react-native-esbuild/shared": "workspace:*"
"@react-native-esbuild/transformer": "workspace:*"
- "@react-native-esbuild/utils": "workspace:*"
"@swc/core": ^1.3.95
chokidar: ^3.5.3
deepmerge: ^4.3.1
esbuild: ^0.19.5
+ esbuild-dependency-graph: ^0.3.0
invariant: ^2.2.4
ora: ^5.4.1
peerDependencies:
@@ -3670,11 +3663,11 @@ __metadata:
dependencies:
"@faker-js/faker": ^8.1.0
"@react-native-community/cli-server-api": ^11.3.6
- "@react-native-esbuild/config": "workspace:*"
"@react-native-esbuild/core": "workspace:*"
+ "@react-native-esbuild/hmr": "workspace:*"
"@react-native-esbuild/internal": "workspace:*"
+ "@react-native-esbuild/shared": "workspace:*"
"@react-native-esbuild/symbolicate": "workspace:*"
- "@react-native-esbuild/utils": "workspace:*"
"@types/connect": ^3.4.35
"@types/invariant": ^2.2.36
"@types/mime": ^3.0.1
@@ -3702,11 +3695,24 @@ __metadata:
languageName: unknown
linkType: soft
-"@react-native-esbuild/internal@workspace:*, @react-native-esbuild/internal@workspace:packages/internal":
+"@react-native-esbuild/hmr@workspace:*, @react-native-esbuild/hmr@workspace:packages/hmr":
+ version: 0.0.0-use.local
+ resolution: "@react-native-esbuild/hmr@workspace:packages/hmr"
+ dependencies:
+ "@react-native-esbuild/internal": "workspace:*"
+ "@react-native-esbuild/shared": "workspace:*"
+ "@swc/helpers": ^0.5.2
+ "@types/react-refresh": ^0.14.3
+ esbuild: ^0.19.3
+ react-refresh: ^0.14.0
+ ws: ^8.14.2
+ languageName: unknown
+ linkType: soft
+
+"@react-native-esbuild/internal@workspace:*, @react-native-esbuild/internal@workspace:^, @react-native-esbuild/internal@workspace:packages/internal":
version: 0.0.0-use.local
resolution: "@react-native-esbuild/internal@workspace:packages/internal"
dependencies:
- "@react-native-esbuild/config": "workspace:*"
esbuild: ^0.19.5
indent-string: ^4.0.0
languageName: unknown
@@ -3720,6 +3726,7 @@ __metadata:
"@jest/transform": ^29.7.0
"@react-native-esbuild/core": "workspace:*"
"@react-native-esbuild/internal": "workspace:*"
+ "@react-native-esbuild/shared": "workspace:*"
"@react-native-esbuild/transformer": "workspace:*"
"@types/md5": ^2.3.2
esbuild: ^0.19.5
@@ -3734,20 +3741,37 @@ __metadata:
resolution: "@react-native-esbuild/plugins@workspace:packages/plugins"
dependencies:
"@faker-js/faker": ^8.1.0
- "@react-native-esbuild/config": "workspace:*"
- "@react-native-esbuild/core": "workspace:*"
+ "@react-native-esbuild/hmr": "workspace:*"
"@react-native-esbuild/internal": "workspace:*"
+ "@react-native-esbuild/shared": "workspace:*"
"@react-native-esbuild/transformer": "workspace:*"
- "@react-native-esbuild/utils": "workspace:*"
"@svgr/core": ^8.1.0
"@svgr/plugin-jsx": ^8.1.0
"@types/invariant": ^2.2.36
"@types/md5": ^2.3.2
deepmerge: ^4.3.1
esbuild: ^0.19.5
+ esbuild-plugin-module-id: ^0.1.3
image-size: ^1.0.2
invariant: ^2.2.4
md5: ^2.3.0
+ ora: ^5.4.1
+ languageName: unknown
+ linkType: soft
+
+"@react-native-esbuild/shared@workspace:*, @react-native-esbuild/shared@workspace:packages/shared":
+ version: 0.0.0-use.local
+ resolution: "@react-native-esbuild/shared@workspace:packages/shared"
+ dependencies:
+ "@babel/core": ^7.23.2
+ "@faker-js/faker": ^8.1.0
+ "@swc/core": ^1.3.95
+ "@types/md5": ^2.3.5
+ colors: ^1.4.0
+ dayjs: ^1.11.10
+ esbuild: ^0.19.5
+ md5: ^2.3.0
+ node-self: ^1.0.2
languageName: unknown
linkType: soft
@@ -3766,30 +3790,20 @@ __metadata:
resolution: "@react-native-esbuild/transformer@workspace:packages/transformer"
dependencies:
"@babel/core": ^7.23.2
- "@react-native-esbuild/config": "workspace:*"
+ "@react-native-esbuild/hmr": "workspace:*"
+ "@react-native-esbuild/internal": "workspace:^"
+ "@react-native-esbuild/shared": "workspace:*"
"@swc/core": ^1.3.95
"@swc/helpers": ^0.5.3
- "@types/md5": ^2.3.4
esbuild: ^0.19.5
- md5: ^2.3.0
+ esbuild-dependency-graph: ^0.3.0
sucrase: ^3.34.0
swc-plugin-coverage-instrument: ^0.0.20
+ swc-plugin-global-module: ^0.1.0-alpha.5
swc_mut_cjs_exports: ^0.85.0
languageName: unknown
linkType: soft
-"@react-native-esbuild/utils@workspace:*, @react-native-esbuild/utils@workspace:packages/utils":
- version: 0.0.0-use.local
- resolution: "@react-native-esbuild/utils@workspace:packages/utils"
- dependencies:
- "@faker-js/faker": ^8.1.0
- colors: ^1.4.0
- dayjs: ^1.11.10
- esbuild: ^0.19.5
- node-self: ^1.0.2
- languageName: unknown
- linkType: soft
-
"@react-native/assets-registry@npm:^0.72.0":
version: 0.72.0
resolution: "@react-native/assets-registry@npm:0.72.0"
@@ -4414,7 +4428,7 @@ __metadata:
languageName: node
linkType: hard
-"@swc/helpers@npm:^0.5.3":
+"@swc/helpers@npm:^0.5.2, @swc/helpers@npm:^0.5.3":
version: 0.5.3
resolution: "@swc/helpers@npm:0.5.3"
dependencies:
@@ -4541,7 +4555,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/babel__core@npm:^7.1.14":
+"@types/babel__core@npm:*, @types/babel__core@npm:^7.1.14":
version: 7.20.3
resolution: "@types/babel__core@npm:7.20.3"
dependencies:
@@ -4743,13 +4757,20 @@ __metadata:
languageName: node
linkType: hard
-"@types/md5@npm:^2.3.2, @types/md5@npm:^2.3.4":
+"@types/md5@npm:^2.3.2":
version: 2.3.4
resolution: "@types/md5@npm:2.3.4"
checksum: 9a724277b2e4b5c6fce4d34d2e2baab821ef47904eccdcbd389fb9973f2e5198f71a4aa98ddcf092fe32457be307d2f3d3f1e099a53d1642c6e569ade5450548
languageName: node
linkType: hard
+"@types/md5@npm:^2.3.5":
+ version: 2.3.5
+ resolution: "@types/md5@npm:2.3.5"
+ checksum: a86baf0521006e3072488bd79089b84831780866102e5e4b4f7afabfab17e0270a3791f3331776b73efb2cc9317efd56a334fd3d2698c7929e9b18593ca3fd39
+ languageName: node
+ linkType: hard
+
"@types/mdast@npm:^3.0.0":
version: 3.0.15
resolution: "@types/mdast@npm:3.0.15"
@@ -4840,6 +4861,16 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-refresh@npm:^0.14.3":
+ version: 0.14.3
+ resolution: "@types/react-refresh@npm:0.14.3"
+ dependencies:
+ "@types/babel__core": "*"
+ csstype: ^3.0.2
+ checksum: ff48c18f928f213cfd1fa379f6b275e401adad87f909a83501a7026075a1d596ef2697269a9ce4fc5063841622ea2c2ffe9c332a2cc9e5ca67199f05c7994f89
+ languageName: node
+ linkType: hard
+
"@types/react-test-renderer@npm:^18.0.5":
version: 18.0.5
resolution: "@types/react-test-renderer@npm:18.0.5"
@@ -8201,7 +8232,21 @@ __metadata:
languageName: node
linkType: hard
-"esbuild@npm:^0.19.5":
+"esbuild-dependency-graph@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "esbuild-dependency-graph@npm:0.3.0"
+ checksum: 831634605db2efaf5ee321b2db13fd5a832a47bf7b0f5e769471ae0f83082841448f55027e4f87a0e1a92e25615fff27ca96f187912e1e6422128d7d74fd7678
+ languageName: node
+ linkType: hard
+
+"esbuild-plugin-module-id@npm:^0.1.3":
+ version: 0.1.3
+ resolution: "esbuild-plugin-module-id@npm:0.1.3"
+ checksum: 7ae649388483063b426ea7c4be985e12cf0f992d3504a41d204303be403c61e0876e10b655e73a3243c4ed40eb6059a9143d9135a374de44a32bb65d0c167d31
+ languageName: node
+ linkType: hard
+
+"esbuild@npm:^0.19.3, esbuild@npm:^0.19.5":
version: 0.19.5
resolution: "esbuild@npm:0.19.5"
dependencies:
@@ -17694,6 +17739,13 @@ __metadata:
languageName: node
linkType: hard
+"swc-plugin-global-module@npm:^0.1.0-alpha.5":
+ version: 0.1.0-alpha.5
+ resolution: "swc-plugin-global-module@npm:0.1.0-alpha.5"
+ checksum: 10523c54644544a989bdd77cf45a273f529d191816d8c5a4cb992267845048d66125e720a71bca49ae2ab47fcd051077f024d00cf4cf31030eeac1c67082ac18
+ languageName: node
+ linkType: hard
+
"swc_mut_cjs_exports@npm:^0.85.0":
version: 0.85.0
resolution: "swc_mut_cjs_exports@npm:0.85.0"
@@ -19122,7 +19174,7 @@ __metadata:
languageName: node
linkType: hard
-"ws@npm:^8.13.0":
+"ws@npm:^8.13.0, ws@npm:^8.14.2":
version: 8.14.2
resolution: "ws@npm:8.14.2"
peerDependencies: