diff --git a/.eslintrc.js b/.eslintrc.js
index bf4642bb..5edc3d8f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -51,6 +51,17 @@ 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-member-access': 'off',
+ '@typescript-eslint/no-unsafe-argument': 'off',
+ '@typescript-eslint/no-unsafe-assignment': 'off',
+ },
+ },
{
files: ['packages/jest/lib/**/*.ts'],
rules: {
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.2.1-00e55c5a7d-b2de0328dd.zip b/.yarn/cache/esbuild-dependency-graph-npm-0.2.1-00e55c5a7d-b2de0328dd.zip
new file mode 100644
index 00000000..fe031d3c
Binary files /dev/null and b/.yarn/cache/esbuild-dependency-graph-npm-0.2.1-00e55c5a7d-b2de0328dd.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..c135d96a 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
@@ -209,6 +213,17 @@ interface Config {
* Additional Esbuild plugins.
*/
plugins?: EsbuildPlugin[];
+ /**
+ * Experimental configurations
+ */
+ experimental?: {
+ /**
+ * Enable HMR(Hot Module Replacement) on development mode.
+ *
+ * Defaults to `false`.
+ */
+ hmr?: boolean;
+ };
/**
* Client event receiver
*/
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..904d3f3f 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -70,10 +70,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..aab752fb 100644
--- a/packages/cli/lib/cli.ts
+++ b/packages/cli/lib/cli.ts
@@ -1,10 +1,6 @@
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
-import {
- DEFAULT_ENTRY_POINT,
- DEFAULT_WEB_ENTRY_POINT,
-} from '@react-native-esbuild/config';
-import { VERSION } from './constants';
+import { constants } from '@react-native-esbuild/shared';
import type { RawArgv } from './types';
const commonOptions = {
@@ -27,7 +23,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',
@@ -79,7 +75,7 @@ export const cli = (): RawArgv | Promise => {
'entry-file': {
type: 'string',
describe: 'Set the entry file path',
- default: DEFAULT_WEB_ENTRY_POINT,
+ default: constants.DEFAULT_WEB_ENTRY_POINT,
},
host: {
describe: 'Set the server host',
@@ -124,7 +120,7 @@ export const cli = (): RawArgv | Promise => {
'entry-file': {
type: 'string',
describe: 'Set the entry file path',
- default: DEFAULT_ENTRY_POINT,
+ default: constants.DEFAULT_ENTRY_POINT,
},
'bundle-output': {
type: 'string',
diff --git a/packages/cli/lib/commands/bundle.ts b/packages/cli/lib/commands/bundle.ts
index 407a829c..35e7f6fb 100644
--- a/packages/cli/lib/commands/bundle.ts
+++ b/packages/cli/lib/commands/bundle.ts
@@ -3,7 +3,7 @@ import { ReactNativeEsbuildBundler } from '@react-native-esbuild/core';
import {
DEFAULT_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';
diff --git a/packages/cli/lib/commands/serve.ts b/packages/cli/lib/commands/serve.ts
index b0771fe5..b8b30209 100644
--- a/packages/cli/lib/commands/serve.ts
+++ b/packages/cli/lib/commands/serve.ts
@@ -1,5 +1,5 @@
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';
diff --git a/packages/cli/lib/commands/start.ts b/packages/cli/lib/commands/start.ts
index d94f63b9..c23167d2 100644
--- a/packages/cli/lib/commands/start.ts
+++ b/packages/cli/lib/commands/start.ts
@@ -1,7 +1,7 @@
/* 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';
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/core/lib/bundler/bundler.ts b/packages/core/lib/bundler/bundler.ts
deleted file mode 100644
index 5146daa3..00000000
--- a/packages/core/lib/bundler/bundler.ts
+++ /dev/null
@@ -1,457 +0,0 @@
-import path from 'node:path';
-import esbuild, {
- type BuildOptions,
- type BuildResult,
- type ServeResult,
-} from 'esbuild';
-import invariant from 'invariant';
-import ora from 'ora';
-import { getGlobalVariables } from '@react-native-esbuild/internal';
-import {
- setEnvironment,
- combineWithDefaultBundleOptions,
- getIdByOptions,
- getDevServerPublicPath,
- type BundleOptions,
-} from '@react-native-esbuild/config';
-import { Logger, LogLevel } from '@react-native-esbuild/utils';
-import { FileSystemWatcher } from '../watcher';
-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,
- getConfigFromGlobal,
- createPromiseHandler,
- getTransformedPreludeScript,
- getResolveExtensionsOption,
- getLoaderOption,
- getEsbuildWebConfig,
-} from './helpers';
-import { printLogo, printVersion } from './logo';
-
-export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
- public static caches = new CacheStorage();
- public static shared = new SharedStorage();
- private appLogger = new Logger('app', LogLevel.Trace);
- private buildTasks = new Map();
- private plugins: ReactNativeEsbuildPluginCreator[] = [];
- private initialized = false;
- private config: Config;
-
- /**
- * Must be bootstrapped first at the entry point
- */
- public static bootstrap(configFilePath?: string): void {
- // Skip printing the logo in the Jest worker process.
- if (process.env.JEST_WORKER_ID === undefined) {
- printLogo();
- printVersion();
- }
-
- const config = loadConfig(configFilePath);
- config.logger?.disabled ?? false ? Logger.disable() : Logger.enable();
- Logger.setTimestampFormat(config.logger?.timestamp ?? null);
-
- invariant(
- config.resolver?.mainFields,
- 'resolver configuration is required',
- );
- invariant(config.transformer, 'transformer configuration is required');
-
- if (!config.resolver.mainFields.includes('react-native')) {
- logger.warn('`react-native` not found in `resolver.mainFields`');
- }
-
- if (!config.transformer.stripFlowPackageNames?.includes('react-native')) {
- logger.warn('`react-native` not found in `stripFlowPackageNames`');
- }
- }
-
- public static getConfig(): Config {
- return getConfigFromGlobal();
- }
-
- public static setGlobalLogLevel(logLevel: LogLevel): void {
- Logger.setGlobalLogLevel(logLevel);
- }
-
- public static async resetCache(): Promise {
- await ReactNativeEsbuildBundler.caches.clearAll();
- logger.info('transform cache was reset');
- }
-
- constructor(private root: string = process.cwd()) {
- super();
- this.config = getConfigFromGlobal();
- this.on('report', (event) => {
- this.broadcastToReporter(event);
- });
- }
-
- private broadcastToReporter(event: ReportableEvent): void {
- // default reporter (for logging)
- 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[]));
- return;
- } else if (event.level === 'groupEnd') {
- this.appLogger.groupEnd();
- return;
- }
-
- this.appLogger[event.level as keyof Logger](
- // @ts-expect-error this.appLogger[event.logger] is logger function
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow any type.
- (event.data as any[]).join(' '),
- );
- 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,
- bundleOptions: BundleOptions,
- additionalData?: BundlerAdditionalData,
- ): 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.sourceExtensions, 'invalid sourceExtensions');
-
- setEnvironment(bundleOptions.dev);
-
- const webSpecifiedOptions =
- bundleOptions.platform === 'web'
- ? getEsbuildWebConfig(mode, this.root, bundleOptions)
- : null;
-
- if (webSpecifiedOptions) {
- bundleOptions.outfile =
- webSpecifiedOptions.outfile ?? path.basename(bundleOptions.entry);
- }
-
- const context: PluginContext = {
- ...bundleOptions,
- id: this.identifyTaskByBundleOptions(bundleOptions),
- root: this.root,
- config: this.config,
- mode,
- additionalData,
- };
-
- return {
- entryPoints: [bundleOptions.entry],
- outfile: bundleOptions.outfile,
- sourceRoot: path.dirname(bundleOptions.entry),
- mainFields: config.resolver.mainFields,
- resolveExtensions: getResolveExtensionsOption(
- bundleOptions,
- config.resolver.sourceExtensions,
- config.resolver.assetExtensions,
- ),
- loader: getLoaderOption(config.resolver.assetExtensions),
- define: getGlobalVariables(bundleOptions),
- banner: {
- js: await getTransformedPreludeScript(bundleOptions, this.root),
- },
- plugins: [
- createBuildStatusPlugin(context, {
- onStart: this.handleBuildStart.bind(this),
- onUpdate: this.handleBuildStateUpdate.bind(this),
- onEnd: this.handleBuildEnd.bind(this),
- }),
- createMetafilePlugin(context),
- // Added plugin creators.
- ...this.plugins.map((plugin) => plugin(context)),
- // Additional plugins in configuration.
- ...(config.plugins ?? []),
- ],
- legalComments: bundleOptions.dev ? 'inline' : 'none',
- target: 'es6',
- format: 'esm',
- supported: {
- /**
- * To avoid block scope bug on hermes engine.
- *
- * If set `for-of` flag to `false`(unsupported),
- * injected `__copyProps` by esbuild will be not use `let` keyword.
- *
- * @see hermes {@link https://github.com/facebook/hermes/issues/575}
- * @see esbuild {@link https://github.com/evanw/esbuild/blob/v0.19.5/internal/runtime/runtime.go#L199-L213}
- */
- 'for-of': false,
- },
- logLevel: 'silent',
- bundle: true,
- sourcemap: true,
- minify: bundleOptions.minify,
- metafile: bundleOptions.metafile,
- write: mode === '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 {
- 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 });
- }
-
- private handleBuildEnd(
- 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 (data.success) return;
- process.exit(1);
- }
-
- const currentTask = this.buildTasks.get(context.id);
- invariant(currentTask, 'no task');
- const bundleEndedAt = new Date();
- const bundleFilename = context.outfile;
- const bundleSourcemapFilename = `${bundleFilename}.map`;
- const revisionId = bundleEndedAt.getTime().toString();
- const { outputFiles } = data.result;
-
- const findFromOutputFile = (
- filename: string,
- ): ((args: T) => boolean) => {
- return ({ path }: T) =>
- path.endsWith(filename);
- };
-
- try {
- invariant(data.success, 'build failed');
- invariant(outputFiles, 'empty outputFiles');
-
- const bundleOutput = outputFiles.find(findFromOutputFile(bundleFilename));
- const bundleSourcemapOutput = outputFiles.find(
- findFromOutputFile(bundleSourcemapFilename),
- );
- invariant(bundleOutput, 'empty bundle output');
- invariant(bundleSourcemapOutput, 'empty sourcemap output');
-
- currentTask.handler?.resolver?.({
- result: {
- source: bundleOutput.contents,
- sourcemap: bundleSourcemapOutput.contents,
- bundledAt: bundleEndedAt,
- revisionId,
- },
- error: null,
- });
- } catch (error) {
- currentTask.handler?.rejecter?.(error);
- } finally {
- currentTask.status = 'resolved';
- this.emit('build-end', {
- revisionId,
- id: context.id,
- additionalData: context.additionalData,
- });
- }
- }
-
- 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,
- });
- // 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 {
- if (this.initialized) {
- logger.warn('bundler already initialized');
- return this;
- }
-
- // Initialize.
- const spinner = ora({ discardStdin: false }).start(
- 'Bundler initializing...',
- );
-
- if (options?.watcherEnabled) {
- await this.startWatcher();
- }
-
- this.initialized = true;
- spinner.stop();
-
- // Post initialize.
- if (self.shouldResetCache) {
- await ReactNativeEsbuildBundler.resetCache();
- }
-
- return this;
- }
-
- public addPlugin(creator: ReactNativeEsbuildPluginCreator): this {
- this.plugins.push(creator);
- return this;
- }
-
- public async bundle(
- bundleOptions: Partial,
- additionalData?: BundlerAdditionalData,
- ): Promise {
- this.throwIfNotInitialized();
- const buildOptions = await this.getBuildOptionsForBundler(
- 'bundle',
- combineWithDefaultBundleOptions(bundleOptions),
- additionalData,
- );
- return esbuild.build(buildOptions);
- }
-
- public async serve(
- bundleOptions: Partial,
- additionalData?: BundlerAdditionalData,
- ): Promise {
- this.throwIfNotInitialized();
- if (bundleOptions.platform !== 'web') {
- throw new Error('serve mode is only available on web platform');
- }
-
- const buildTask = await this.getOrCreateBundleTask(
- combineWithDefaultBundleOptions(bundleOptions),
- additionalData,
- );
- invariant(buildTask.handler, 'no handler');
-
- return buildTask.context.serve({
- servedir: getDevServerPublicPath(this.root),
- });
- }
-
- public async getBundleResult(
- bundleOptions: BundleRequestOptions,
- additionalData?: BundlerAdditionalData,
- ): Promise {
- this.throwIfNotInitialized();
- const buildTask = await this.getOrCreateBundleTask(
- combineWithDefaultBundleOptions(bundleOptions),
- additionalData,
- );
- invariant(buildTask.handler, 'no handler');
-
- return buildTask.handler.task;
- }
-
- public getRoot(): string {
- return this.root;
- }
-}
diff --git a/packages/core/lib/bundler/events/index.ts b/packages/core/lib/bundler/events/index.ts
index cd3718b7..27473fe6 100644
--- a/packages/core/lib/bundler/events/index.ts
+++ b/packages/core/lib/bundler/events/index.ts
@@ -1,4 +1,5 @@
import EventEmitter from 'node:events';
+import type { BundleUpdate } from '@react-native-esbuild/hmr';
import type {
BundlerAdditionalData,
BuildStatus,
@@ -46,6 +47,7 @@ export interface BundlerEventPayload {
'build-end': {
id: number;
revisionId: string;
+ update: BundleUpdate | null;
additionalData?: BundlerAdditionalData;
};
'build-status-change': BuildStatus & {
diff --git a/packages/core/lib/bundler/index.ts b/packages/core/lib/bundler/index.ts
index a8145c19..f42a73fe 100644
--- a/packages/core/lib/bundler/index.ts
+++ b/packages/core/lib/bundler/index.ts
@@ -1,2 +1,543 @@
-export { ReactNativeEsbuildBundler } from './bundler';
-export type * from './events';
+import path from 'node:path';
+import type { Stats } from 'node:fs';
+import esbuild, {
+ type BuildOptions,
+ type BuildResult,
+ type ServeResult,
+} from 'esbuild';
+import invariant from 'invariant';
+import ora from 'ora';
+import { getGlobalVariables } from '@react-native-esbuild/internal';
+import { HmrTransformer } from '@react-native-esbuild/hmr';
+import { helpers, type BundleOptions } from '@react-native-esbuild/shared';
+import { Logger, LogLevel } from '@react-native-esbuild/shared';
+import { logger } from '../shared';
+import type {
+ Config,
+ BundlerInitializeOptions,
+ BuildTask,
+ BuildStatus,
+ BundleMode,
+ BundlerAdditionalData,
+ BundleResult,
+ BundleRequestOptions,
+ PluginContext,
+ ReportableEvent,
+ ReactNativeEsbuildPluginCreator,
+} from '../types';
+import { printLogo, printVersion } from '../misc';
+import {
+ loadConfig,
+ createPromiseHandler,
+ getConfigFromGlobal,
+ getTransformedPreludeScript,
+ getResolveExtensionsOption,
+ getLoaderOption,
+ getEsbuildWebConfig,
+ getExternalFromPackageJson,
+ getExternalModulePattern,
+} from '../helpers';
+import { FileSystemWatcher } from './watcher';
+import { CacheStorage, SharedStorage } from './storages';
+import {
+ createBuildStatusPlugin,
+ createMetafilePlugin,
+ createModuleIdPlugin,
+} from './plugins';
+import { BundlerEventEmitter } from './events';
+
+export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
+ public static caches = CacheStorage.getInstance();
+ public static shared = SharedStorage.getInstance();
+ private static hmr = new Map();
+ private appLogger = new Logger('app', LogLevel.Trace);
+ private buildTasks = new Map();
+ private plugins: ReactNativeEsbuildPluginCreator[] = [];
+ private config: Config;
+ private external: string[];
+ private externalPattern: string;
+ private initialized = false;
+
+ /**
+ * Must be bootstrapped first at the entry point
+ */
+ public static bootstrap(configFilePath?: string): void {
+ // Skip printing the logo in the Jest worker process.
+ if (process.env.JEST_WORKER_ID === undefined) {
+ printLogo();
+ printVersion();
+ }
+
+ const config = loadConfig(configFilePath);
+ config.logger?.disabled ?? false ? Logger.disable() : Logger.enable();
+ Logger.setTimestampFormat(config.logger?.timestamp ?? null);
+
+ invariant(
+ config.resolver?.mainFields,
+ 'resolver configuration is required',
+ );
+ invariant(config.transformer, 'transformer configuration is required');
+
+ if (!config.resolver.mainFields.includes('react-native')) {
+ logger.warn('`react-native` not found in `resolver.mainFields`');
+ }
+
+ if (!config.transformer.stripFlowPackageNames?.includes('react-native')) {
+ logger.warn('`react-native` not found in `stripFlowPackageNames`');
+ }
+ }
+
+ public static getConfig(): Config {
+ return getConfigFromGlobal();
+ }
+
+ public static setGlobalLogLevel(logLevel: LogLevel): void {
+ Logger.setGlobalLogLevel(logLevel);
+ }
+
+ public static async resetCache(): Promise {
+ await ReactNativeEsbuildBundler.caches.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)
+ switch (event.type) {
+ case 'client_log': {
+ if (event.level === 'group' || event.level === 'groupCollapsed') {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any -- allow
+ this.appLogger.group(...(event.data as unknown as any[]));
+ return;
+ } else if (event.level === 'groupEnd') {
+ this.appLogger.groupEnd();
+ return;
+ }
+
+ this.appLogger[event.level as keyof Logger](
+ // @ts-expect-error this.appLogger[event.logger] is logger function
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow any type.
+ (event.data as any[]).join(' '),
+ );
+ break;
+ }
+ }
+
+ // send event to custom reporter
+ this.config.reporter?.(event);
+ }
+
+ private startWatcher(): Promise {
+ return FileSystemWatcher.getInstance()
+ .setHandler(this.handleFileChanged.bind(this))
+ .watch(this.root);
+ }
+
+ private async getBuildOptionsForBundler(
+ mode: BundleMode,
+ bundleOptions: BundleOptions,
+ additionalData?: BundlerAdditionalData,
+ ): 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 assetExtensions');
+ invariant(config.resolver.sourceExtensions, 'invalid sourceExtensions');
+ helpers.setEnvironment(bundleOptions.dev);
+
+ const enableHmr = Boolean(
+ mode === 'watch' && bundleOptions.dev && config.experimental?.hmr,
+ );
+ const webSpecifiedOptions =
+ bundleOptions.platform === 'web'
+ ? getEsbuildWebConfig(mode, this.root, bundleOptions)
+ : null;
+
+ if (webSpecifiedOptions) {
+ bundleOptions.outfile =
+ webSpecifiedOptions.outfile ?? path.basename(bundleOptions.entry);
+ }
+
+ const context: PluginContext = {
+ ...bundleOptions,
+ id: this.identifyTaskByBundleOptions(bundleOptions),
+ root: this.root,
+ config: this.config,
+ externalPattern: this.externalPattern,
+ enableHmr,
+ mode,
+ additionalData,
+ };
+
+ return {
+ entryPoints: [bundleOptions.entry],
+ outfile: bundleOptions.outfile,
+ sourceRoot: this.root,
+ mainFields: config.resolver.mainFields,
+ resolveExtensions: getResolveExtensionsOption(
+ bundleOptions,
+ config.resolver.sourceExtensions,
+ config.resolver.assetExtensions,
+ ),
+ loader: getLoaderOption(config.resolver.assetExtensions),
+ define: getGlobalVariables(bundleOptions),
+ banner: {
+ js: await getTransformedPreludeScript(
+ bundleOptions,
+ this.root,
+ [enableHmr ? '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),
+ }),
+ createModuleIdPlugin(context),
+ createMetafilePlugin(context),
+ // Added plugin creators.
+ ...this.plugins.map((plugin) => plugin(context)),
+ // Additional plugins in configuration.
+ ...(config.plugins ?? []),
+ ],
+ legalComments: 'none',
+ target: 'es6',
+ format: 'esm',
+ supported: {
+ /**
+ * To avoid block scope bug on hermes engine.
+ *
+ * If set `for-of` flag to `false`(unsupported),
+ * injected `__copyProps` by esbuild will be not use `let` keyword.
+ *
+ * @see hermes {@link https://github.com/facebook/hermes/issues/575}
+ * @see esbuild {@link https://github.com/evanw/esbuild/blob/v0.19.5/internal/runtime/runtime.go#L199-L213}
+ */
+ 'for-of': false,
+ },
+ logLevel: 'silent',
+ bundle: true,
+ sourcemap: true,
+ metafile: true,
+ minify: bundleOptions.minify,
+ write: mode === 'bundle',
+ ...webSpecifiedOptions,
+ };
+ }
+
+ private identifyTaskByBundleOptions(bundleOptions: BundleOptions): number {
+ return helpers.getIdByOptions(bundleOptions);
+ }
+
+ private throwIfNotInitialized(): void {
+ if (this.initialized) return;
+ throw new Error('bundler not initialized');
+ }
+
+ private handleBuildStart(context: PluginContext): 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 });
+ }
+
+ private handleBuildEnd(
+ data: { result: BuildResult; success: boolean },
+ context: PluginContext,
+ ): void {
+ invariant(data.result.metafile, 'invalid metafile');
+
+ /**
+ * Exit at the end of a build in bundle mode.
+ *
+ * If the build fails, exit with status 1.
+ */
+ if (context.mode === 'bundle') {
+ if (data.success) return;
+ process.exit(1);
+ }
+
+ const hmrController = ReactNativeEsbuildBundler.hmr.get(context.id);
+ const currentTask = this.buildTasks.get(context.id);
+ invariant(currentTask, 'no task');
+ invariant(hmrController, 'no hmr controller');
+
+ if (context.enableHmr) {
+ hmrController.initializeDependencyGraph(data.result.metafile);
+ }
+
+ const bundleEndedAt = new Date();
+ const bundleFilename = context.outfile;
+ const bundleSourcemapFilename = `${bundleFilename}.map`;
+ const revisionId = bundleEndedAt.getTime().toString();
+ const { outputFiles } = data.result;
+
+ const findFromOutputFile = (
+ filename: string,
+ ): ((args: T) => boolean) => {
+ return ({ path }: T) =>
+ path.endsWith(filename);
+ };
+
+ try {
+ invariant(data.success, 'build failed');
+ invariant(outputFiles, 'empty outputFiles');
+
+ const bundleOutput = outputFiles.find(findFromOutputFile(bundleFilename));
+ const bundleSourcemapOutput = outputFiles.find(
+ findFromOutputFile(bundleSourcemapFilename),
+ );
+ invariant(bundleOutput, 'empty bundle output');
+ invariant(bundleSourcemapOutput, 'empty sourcemap output');
+
+ currentTask.handler?.resolver?.({
+ result: {
+ source: bundleOutput.contents,
+ sourcemap: bundleSourcemapOutput.contents,
+ bundledAt: bundleEndedAt,
+ revisionId,
+ },
+ error: null,
+ });
+ } catch (error) {
+ currentTask.handler?.rejecter?.(error);
+ } finally {
+ currentTask.status = 'resolved';
+ this.emit('build-end', {
+ revisionId,
+ id: context.id,
+ additionalData: context.additionalData,
+ update: null,
+ });
+ }
+ }
+
+ private handleFileChanged(
+ event: string,
+ changedFile: string,
+ _stats?: Stats,
+ ): void {
+ const hasTask = this.buildTasks.size > 0;
+ const isChanged = event === 'change';
+ if (!(hasTask && 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 &&
+ HmrTransformer.isBoundary(changedFile)
+ ) {
+ 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(({ context }) => {
+ context.rebuild();
+ });
+ }
+ }
+
+ private async getOrSetupTask(
+ bundleOptions: BundleOptions,
+ additionalData?: BundlerAdditionalData,
+ ): Promise {
+ const targetTaskId = this.identifyTaskByBundleOptions(bundleOptions);
+
+ // Build Task
+ 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,
+ });
+ logger.debug(`bundle task is now watching (id: ${targetTaskId})`);
+ }
+
+ // HMR Transformer
+ if (
+ this.config.experimental?.hmr &&
+ !ReactNativeEsbuildBundler.hmr.has(targetTaskId)
+ ) {
+ const {
+ stripFlowPackageNames,
+ fullyTransformPackageNames,
+ additionalTransformRules,
+ } = this.config.transformer ?? {};
+ ReactNativeEsbuildBundler.hmr.set(
+ targetTaskId,
+ new HmrTransformer(
+ {
+ ...bundleOptions,
+ id: targetTaskId,
+ root: this.root,
+ externalPattern: this.externalPattern,
+ },
+ {
+ additionalBabelRules: additionalTransformRules?.babel,
+ additionalSwcRules: additionalTransformRules?.swc,
+ fullyTransformPackageNames,
+ stripFlowPackageNames,
+ },
+ ),
+ );
+ }
+
+ // 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,
+ });
+
+ /**
+ * Set status to `pending` and create new handler when it is stale.
+ *
+ * - `buildCount` is 0, it is first build.
+ * - `buildCount` is greater than 0, create new handler (handler is stale).
+ */
+ targetTask.handler =
+ targetTask.buildCount === 0 ? targetTask.handler : createPromiseHandler();
+ targetTask.status = 'pending';
+ targetTask.buildCount += 1;
+ }
+
+ public async initialize(options?: BundlerInitializeOptions): Promise {
+ if (this.initialized) {
+ logger.warn('bundler already initialized');
+ return this;
+ }
+
+ // Initialize.
+ const spinner = ora({ discardStdin: false }).start(
+ 'Bundler initializing...',
+ );
+
+ if (options?.watcherEnabled) {
+ await this.startWatcher();
+ }
+
+ this.initialized = true;
+ spinner.stop();
+
+ // Post initialize.
+ if (self.shouldResetCache) {
+ await ReactNativeEsbuildBundler.resetCache();
+ }
+
+ return this;
+ }
+
+ public addPlugin(creator: ReactNativeEsbuildPluginCreator): this {
+ this.plugins.push(creator);
+ return this;
+ }
+
+ public async bundle(
+ bundleOptions: Partial,
+ additionalData?: BundlerAdditionalData,
+ ): Promise {
+ this.throwIfNotInitialized();
+ const buildOptions = await this.getBuildOptionsForBundler(
+ 'bundle',
+ helpers.combineWithDefaultBundleOptions(bundleOptions),
+ additionalData,
+ );
+ return esbuild.build(buildOptions);
+ }
+
+ public async serve(
+ bundleOptions: Partial,
+ additionalData?: BundlerAdditionalData,
+ ): Promise {
+ this.throwIfNotInitialized();
+ if (bundleOptions.platform !== 'web') {
+ throw new Error('serve mode is only available on web platform');
+ }
+
+ const buildTask = await this.getOrSetupTask(
+ helpers.combineWithDefaultBundleOptions(bundleOptions),
+ additionalData,
+ );
+
+ if (buildTask.status === 'pending') {
+ buildTask.context.rebuild();
+ }
+
+ invariant(buildTask.handler, 'no handler');
+
+ return buildTask.context.serve({
+ servedir: helpers.getDevServerPublicPath(this.root),
+ });
+ }
+
+ public async getBundleResult(
+ bundleOptions: BundleRequestOptions,
+ additionalData?: BundlerAdditionalData,
+ ): Promise {
+ this.throwIfNotInitialized();
+ const buildTask = await this.getOrSetupTask(
+ helpers.combineWithDefaultBundleOptions(bundleOptions),
+ additionalData,
+ );
+
+ if (buildTask.status === 'pending') {
+ buildTask.context.rebuild();
+ }
+
+ invariant(buildTask.handler, 'no handler');
+
+ return buildTask.handler.task;
+ }
+
+ public getRoot(): string {
+ return this.root;
+ }
+}
diff --git a/packages/core/lib/bundler/plugins/index.ts b/packages/core/lib/bundler/plugins/index.ts
index 393e30c9..35f008eb 100644
--- a/packages/core/lib/bundler/plugins/index.ts
+++ b/packages/core/lib/bundler/plugins/index.ts
@@ -1,2 +1,3 @@
export { createMetafilePlugin } from './metafilePlugin';
+export { createModuleIdPlugin } from './moduleIdPlugin';
export { createBuildStatusPlugin } from './statusPlugin';
diff --git a/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts b/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts
index 70bb8c1b..96e17ab1 100644
--- a/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts
+++ b/packages/core/lib/bundler/plugins/metafilePlugin/metafilePlugin.ts
@@ -12,18 +12,16 @@ export const createMetafilePlugin: ReactNativeEsbuildPluginCreator = (
name: NAME,
setup: (build): void => {
build.onEnd(async (result: BuildResult) => {
- const { metafile } = result;
+ if (!(context.metafile && result.metafile)) return;
+
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',
- });
- }
+ logger.debug('writing esbuild metafile', { destination: filename });
+ await fs.writeFile(filename, JSON.stringify(result.metafile), {
+ encoding: 'utf-8',
+ });
});
},
});
diff --git a/packages/core/lib/bundler/plugins/moduleIdPlugin/ModuleIdManager.ts b/packages/core/lib/bundler/plugins/moduleIdPlugin/ModuleIdManager.ts
new file mode 100644
index 00000000..37ce4b0d
--- /dev/null
+++ b/packages/core/lib/bundler/plugins/moduleIdPlugin/ModuleIdManager.ts
@@ -0,0 +1,15 @@
+import {
+ DefaultModuleIdGenerator,
+ type ModuleId,
+} from 'esbuild-plugin-module-id';
+import type { OnLoadArgs } from 'esbuild';
+
+export class ModuleIdManager extends DefaultModuleIdGenerator {
+ getId(args: OnLoadArgs): ModuleId {
+ const id = this.INTERNAL_moduleIds[args.path];
+ if (typeof id === 'number') {
+ return id;
+ }
+ throw new Error(`unable to get module id: ${args.path}`);
+ }
+}
diff --git a/packages/core/lib/bundler/plugins/moduleIdPlugin/index.ts b/packages/core/lib/bundler/plugins/moduleIdPlugin/index.ts
new file mode 100644
index 00000000..fe1ee837
--- /dev/null
+++ b/packages/core/lib/bundler/plugins/moduleIdPlugin/index.ts
@@ -0,0 +1 @@
+export { createModuleIdPlugin } from './moduleIdPlugin';
diff --git a/packages/core/lib/bundler/plugins/moduleIdPlugin/moduleIdPlugin.ts b/packages/core/lib/bundler/plugins/moduleIdPlugin/moduleIdPlugin.ts
new file mode 100644
index 00000000..b2d0712f
--- /dev/null
+++ b/packages/core/lib/bundler/plugins/moduleIdPlugin/moduleIdPlugin.ts
@@ -0,0 +1,15 @@
+import { moduleId } from 'esbuild-plugin-module-id';
+import type { ReactNativeEsbuildPluginCreator } from '../../../types';
+import { SharedStorage } from '../../storages';
+import { ModuleIdManager } from './ModuleIdManager';
+
+export const createModuleIdPlugin: ReactNativeEsbuildPluginCreator = (
+ context,
+) => {
+ const shared = SharedStorage.getInstance();
+ const manager = new ModuleIdManager();
+
+ shared.get(context.id).moduleIdManager = manager;
+
+ return moduleId({ generator: manager });
+};
diff --git a/packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts b/packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts
index 0a8c4384..a9f8238b 100644
--- a/packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts
+++ b/packages/core/lib/bundler/plugins/statusPlugin/StatusLogger.ts
@@ -3,10 +3,9 @@ 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 { colors, utils, helpers } from '@react-native-esbuild/shared';
import { logger } from '../../../shared';
-import { ESBUILD_LABEL } from '../../logo';
+import { ESBUILD_LABEL } from '../../../misc/logo';
import type { BuildStatus, PluginContext } from '../../../types';
import { fromTemplate, getSummaryTemplate } from './templates';
@@ -36,7 +35,7 @@ export class StatusLogger {
const loaded = this.loadedModules;
// Enable interactive message when only in a TTY environment.
- if (isTTY()) {
+ if (utils.isTTY()) {
this.totalModuleCount = Math.max(resolved, this.totalModuleCount);
const percent = Math.min(
(loaded / this.totalModuleCount) * 100 || 0,
@@ -64,7 +63,7 @@ export class StatusLogger {
kind: 'warning' | 'error',
): Promise {
const formattedMessages = await esbuild
- .formatMessages(messages, { kind, color: isTTY() })
+ .formatMessages(messages, { kind, color: utils.isTTY() })
.catch((error) => {
logger.error('unable to format error messages', error as Error);
return null;
@@ -103,7 +102,8 @@ export class StatusLogger {
this.previousPercent = 0;
this.statusUpdate();
- isTTY()
+ process.stdout.write('\n');
+ utils.isTTY()
? this.spinner.start()
: this.print(`${this.platformText} build in progress...`);
}
@@ -122,7 +122,7 @@ export class StatusLogger {
? `${this.platformText} done!`
: `${this.platformText} failed!`;
- if (isTTY()) {
+ if (utils.isTTY()) {
isSuccess
? this.spinner.succeed(resultText)
: this.spinner.fail(resultText);
@@ -144,7 +144,7 @@ export class StatusLogger {
loadStatus(): Promise {
return fs
- .readFile(getBuildStatusCachePath(this.context.root), 'utf-8')
+ .readFile(helpers.getBuildStatusCachePath(this.context.root), 'utf-8')
.then((data) => {
const cachedStatus = JSON.parse(data) as unknown as {
totalModuleCount?: number;
@@ -156,7 +156,9 @@ export class StatusLogger {
async persistStatus(): Promise {
try {
- const statusCacheFile = getBuildStatusCachePath(this.context.root);
+ const statusCacheFile = helpers.getBuildStatusCachePath(
+ this.context.root,
+ );
await fs.mkdir(path.dirname(statusCacheFile), { recursive: true });
await fs.writeFile(
statusCacheFile,
diff --git a/packages/core/lib/bundler/plugins/statusPlugin/templates.ts b/packages/core/lib/bundler/plugins/statusPlugin/templates.ts
index 50407223..c725f692 100644
--- a/packages/core/lib/bundler/plugins/statusPlugin/templates.ts
+++ b/packages/core/lib/bundler/plugins/statusPlugin/templates.ts
@@ -1,4 +1,4 @@
-import { isTTY, colors } from '@react-native-esbuild/utils';
+import { colors, utils } from '@react-native-esbuild/shared';
const summaryTemplateForTTY = `
╭───────────╯
@@ -15,7 +15,7 @@ const summaryTemplateForNonTTY = `
export const getSummaryTemplate = (): string => {
return colors.gray(
- isTTY() ? summaryTemplateForTTY : summaryTemplateForNonTTY,
+ utils.isTTY() ? summaryTemplateForTTY : summaryTemplateForNonTTY,
);
};
diff --git a/packages/core/lib/bundler/storages/CacheStorage.ts b/packages/core/lib/bundler/storages/CacheStorage.ts
index c62a94c4..63efe286 100644
--- a/packages/core/lib/bundler/storages/CacheStorage.ts
+++ b/packages/core/lib/bundler/storages/CacheStorage.ts
@@ -1,15 +1,24 @@
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 { constants } from '@react-native-esbuild/shared';
import { logger } from '../../shared';
import { CacheController } from '../cache';
import { Storage } from './Storage';
-const CACHE_DIRECTORY = path.join(os.tmpdir(), GLOBAL_CACHE_DIR);
+const CACHE_DIRECTORY = path.join(os.tmpdir(), constants.GLOBAL_CACHE_DIR);
export class CacheStorage extends Storage {
- constructor() {
+ private static instance: CacheStorage | null = null;
+
+ public static getInstance(): CacheStorage {
+ if (CacheStorage.instance === null) {
+ CacheStorage.instance = new CacheStorage();
+ }
+ return CacheStorage.instance;
+ }
+
+ private constructor() {
super();
try {
fs.accessSync(CACHE_DIRECTORY, fs.constants.R_OK | fs.constants.W_OK);
diff --git a/packages/core/lib/bundler/storages/SharedStorage.ts b/packages/core/lib/bundler/storages/SharedStorage.ts
index 05c2389e..1fb41603 100644
--- a/packages/core/lib/bundler/storages/SharedStorage.ts
+++ b/packages/core/lib/bundler/storages/SharedStorage.ts
@@ -1,42 +1,35 @@
-import type { BundlerSharedData } from '../../types';
+import type { SharedData } from '../../types';
import { Storage } from './Storage';
-export class SharedStorage extends Storage {
- private getDefaultSharedData(): BundlerSharedData {
- return {
- watcher: {
- changed: null,
- stats: undefined,
- },
- };
+export class SharedStorage extends Storage {
+ private static instance: SharedStorage | null = null;
+
+ public static getInstance(): SharedStorage {
+ if (SharedStorage.instance === null) {
+ SharedStorage.instance = new SharedStorage();
+ }
+ return SharedStorage.instance;
}
- public get(key: number): BundlerSharedData {
+ public get(key: number): SharedData {
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);
+ const initialData = {
+ moduleIdManager: undefined,
+ };
- return sharedData;
- }
+ this.data.set(key, initialData);
- 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;
- }
+ return initialData;
}
- public clearAll(): Promise {
- for (const sharedData of this.data.values()) {
- sharedData.watcher.changed = null;
- sharedData.watcher.stats = undefined;
+ // eslint-disable-next-line @typescript-eslint/require-await -- return type.
+ public async clearAll(): Promise {
+ for (const key of this.data.keys()) {
+ this.data.delete(key);
}
- return Promise.resolve();
}
}
diff --git a/packages/core/lib/watcher/FileSystemWatcher.ts b/packages/core/lib/bundler/watcher/FileSystemWatcher.ts
similarity index 98%
rename from packages/core/lib/watcher/FileSystemWatcher.ts
rename to packages/core/lib/bundler/watcher/FileSystemWatcher.ts
index cf167382..50e5d839 100644
--- a/packages/core/lib/watcher/FileSystemWatcher.ts
+++ b/packages/core/lib/bundler/watcher/FileSystemWatcher.ts
@@ -4,7 +4,7 @@ import {
SOURCE_EXTENSIONS,
ASSET_EXTENSIONS,
} from '@react-native-esbuild/internal';
-import { logger } from '../shared';
+import { logger } from '../../shared';
const WATCH_EXTENSIONS_REGEXP = new RegExp(
`(?:${[...SOURCE_EXTENSIONS, ...ASSET_EXTENSIONS].join('|')})$`,
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 96%
rename from packages/core/lib/bundler/helpers/__tests__/config.test.ts
rename to packages/core/lib/helpers/__tests__/config.test.ts
index 22d29158..a72c0983 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 { loadConfig, getConfigFromGlobal } from '../config';
-import type { Config } from '../../../types';
+import type { Config } from '../../types';
describe('config helpers', () => {
describe('loadConfig', () => {
diff --git a/packages/core/lib/bundler/helpers/async.ts b/packages/core/lib/helpers/async.ts
similarity index 87%
rename from packages/core/lib/bundler/helpers/async.ts
rename to packages/core/lib/helpers/async.ts
index 0f8b7606..085fdf16 100644
--- a/packages/core/lib/bundler/helpers/async.ts
+++ b/packages/core/lib/helpers/async.ts
@@ -1,4 +1,4 @@
-import type { BundleResult, PromiseHandler } from '../../types';
+import type { BundleResult, PromiseHandler } from '../types';
export const createPromiseHandler = (): PromiseHandler => {
let resolver: PromiseHandler['resolver'] | undefined;
diff --git a/packages/core/lib/bundler/helpers/config.ts b/packages/core/lib/helpers/config.ts
similarity index 86%
rename from packages/core/lib/bundler/helpers/config.ts
rename to packages/core/lib/helpers/config.ts
index 8c83fd43..4376d43c 100644
--- a/packages/core/lib/bundler/helpers/config.ts
+++ b/packages/core/lib/helpers/config.ts
@@ -6,12 +6,9 @@ import {
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 { helpers, type BundleOptions } from '@react-native-esbuild/shared';
+import { logger } from '../shared';
+import type { BundleMode, Config } from '../types';
export const loadConfig = (configFilePath?: string): Config => {
let config: Config | undefined;
@@ -29,13 +26,6 @@ export const loadConfig = (configFilePath?: string): Config => {
assetExtensions: ASSET_EXTENSIONS,
},
transformer: {
- jsc: {
- transform: {
- react: {
- runtime: 'automatic',
- },
- },
- },
stripFlowPackageNames: ['react-native'],
},
web: {
@@ -97,7 +87,8 @@ export const getEsbuildWebConfig = (
* - bundle mode: `outfile`
* - watch mode:`outdir`
*/
- outdir: mode === 'bundle' ? undefined : getDevServerPublicPath(root),
+ outdir:
+ mode === 'bundle' ? undefined : helpers.getDevServerPublicPath(root),
outfile: mode === '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..2c774537
--- /dev/null
+++ b/packages/core/lib/helpers/fs.ts
@@ -0,0 +1,14 @@
+import fs from 'node:fs';
+import path from 'node:path';
+
+export const getExternalFromPackageJson = (root: string): string[] => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- allow any.
+ 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 78%
rename from packages/core/lib/bundler/helpers/index.ts
rename to packages/core/lib/helpers/index.ts
index dd49165c..ad840df1 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 './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 58%
rename from packages/core/lib/bundler/helpers/internal.ts
rename to packages/core/lib/helpers/internal.ts
index 5955e7f3..477f1ec8 100644
--- a/packages/core/lib/bundler/helpers/internal.ts
+++ b/packages/core/lib/helpers/internal.ts
@@ -1,16 +1,21 @@
+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 {
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';
+
+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 = {
@@ -20,19 +25,28 @@ export const getTransformedPreludeScript = async (
dev: bundleOptions.dev,
entry: bundleOptions.entry,
};
- const preludeScript = await getPreludeScript(bundleOptions, root);
+
+ const additionalPreludeScripts = await Promise.all(
+ (additionalScriptPaths ?? []).map(loadScript),
+ );
+
+ const preludeScript = [
+ await getPreludeScript(bundleOptions, 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 +84,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..9055d88c 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 * from './types';
-export type * from './bundler';
export type { CacheController } from './bundler/cache';
+export type * from './bundler/events';
+export type * from './types';
diff --git a/packages/core/lib/misc/index.ts b/packages/core/lib/misc/index.ts
new file mode 100644
index 00000000..cb6151df
--- /dev/null
+++ b/packages/core/lib/misc/index.ts
@@ -0,0 +1 @@
+export * from './logo';
diff --git a/packages/core/lib/bundler/logo.ts b/packages/core/lib/misc/logo.ts
similarity index 84%
rename from packages/core/lib/bundler/logo.ts
rename to packages/core/lib/misc/logo.ts
index 64e2cc26..34f33089 100644
--- a/packages/core/lib/bundler/logo.ts
+++ b/packages/core/lib/misc/logo.ts
@@ -1,4 +1,4 @@
-import { colors, isTTY } from '@react-native-esbuild/utils';
+import { colors, utils } from '@react-native-esbuild/shared';
const LOGO = `
"88e "88e
@@ -16,7 +16,7 @@ export const ESBUILD_LABEL = ' » esbuild ';
const DESCRIPTION = 'An extremely fast bundler';
export const printLogo = (): void => {
- if (isTTY()) {
+ if (utils.isTTY()) {
process.stdout.write(`${colors.yellow(LOGO)}\n`);
process.stdout.write(
[
@@ -37,6 +37,6 @@ export const printVersion = (): void => {
.fill(' ')
.join('');
process.stdout.write(
- `${isTTY() ? paddingForCenterAlign : ''}v${self._version}\n\n`,
+ `${utils.isTTY() ? paddingForCenterAlign : ''}v${self._version}\n\n`,
);
};
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..eadf6e32 100644
--- a/packages/core/lib/types.ts
+++ b/packages/core/lib/types.ts
@@ -1,11 +1,10 @@
-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';
+import type { BundleOptions } from '@react-native-esbuild/shared';
+import type { ModuleIdManager } from './bundler/plugins/moduleIdPlugin/ModuleIdManager';
export interface Config {
/**
@@ -58,10 +57,6 @@ export interface Config {
* Transformer configurations
*/
transformer?: {
- /**
- * Swc's `jsc` config.
- */
- jsc: Pick;
/**
* Strip flow syntax.
*
@@ -125,6 +120,17 @@ export interface Config {
* Additional Esbuild plugins.
*/
plugins?: Plugin[];
+ /**
+ * Experimental configurations
+ */
+ experimental?: {
+ /**
+ * Enable HMR(Hot Module Replacement) on development mode.
+ *
+ * Defaults to `false`.
+ */
+ hmr?: boolean;
+ };
/**
* Client event receiver (only work on native)
*/
@@ -166,13 +172,6 @@ export type ReactNativeEsbuildPluginCreator = (
config?: PluginConfig,
) => Plugin;
-export interface BundlerSharedData {
- watcher: {
- changed: string | null;
- stats?: Stats;
- };
-}
-
export type BundlerAdditionalData = Record;
export interface PluginContext extends BundleOptions {
@@ -180,6 +179,8 @@ export interface PluginContext extends BundleOptions {
root: string;
config: Config;
mode: BundleMode;
+ enableHmr: boolean;
+ externalPattern: string;
additionalData?: BundlerAdditionalData;
}
@@ -202,6 +203,10 @@ export interface Cache {
modifiedAt: number;
}
+export interface SharedData {
+ moduleIdManager: ModuleIdManager | undefined;
+}
+
export type ReportableEvent = ClientLogEvent;
/**
diff --git a/packages/core/package.json b/packages/core/package.json
index 7005cc38..824d9e71 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -40,18 +40,20 @@
"node": ">=16"
},
"dependencies": {
- "@react-native-esbuild/config": "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:*",
"chokidar": "^3.5.3",
"deepmerge": "^4.3.1",
"esbuild": "^0.19.5",
+ "esbuild-plugin-module-id": "^0.1.3",
"invariant": "^2.2.4",
"ora": "^5.4.1"
},
"peerDependencies": {
- "react-native": "*"
+ "react-native": "*",
+ "swc-plugin-global-module": "*"
},
"devDependencies": {
"@babel/core": "^7.23.2",
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..837f449f 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 { constants } from '@react-native-esbuild/shared';
import { faker } from '@faker-js/faker';
import { createServeAssetMiddleware } from '../serveAsset';
import type { DevServerMiddleware } from '../../types';
@@ -51,7 +51,10 @@ describe('serve-asset-middleware', () => {
let next: jest.Mock;
beforeEach(() => {
- assetRequestUrl = path.join(ASSET_PATH, faker.system.fileName());
+ assetRequestUrl = path.join(
+ constants.ASSET_PATH,
+ faker.system.fileName(),
+ );
request = getMockedRequest({ url: `/${assetRequestUrl}` });
response = getMockedResponse();
next = jest.fn();
diff --git a/packages/dev-server/lib/middlewares/hmr.ts b/packages/dev-server/lib/middlewares/hmr.ts
new file mode 100644
index 00000000..42c26f32
--- /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..cc15e0e3 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 { constants } from '@react-native-esbuild/shared';
import { logger } from '../shared';
import type { DevServerMiddlewareCreator } from '../types';
@@ -43,7 +43,7 @@ export const createServeAssetMiddleware: DevServerMiddlewareCreator = (
if (
!(
typeof request.url === 'string' &&
- request.url.startsWith(`/${ASSET_PATH}`)
+ request.url.startsWith(`/${constants.ASSET_PATH}`)
)
) {
next();
@@ -51,7 +51,7 @@ export const createServeAssetMiddleware: DevServerMiddlewareCreator = (
}
const { pathname, query } = url.parse(
- request.url.replace(new RegExp(`^/${ASSET_PATH}`), ''),
+ request.url.replace(new RegExp(`^/${constants.ASSET_PATH}`), ''),
true,
);
diff --git a/packages/dev-server/lib/middlewares/serveBundle.ts b/packages/dev-server/lib/middlewares/serveBundle.ts
index 84c85939..29261041 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 { helpers } from '@react-native-esbuild/shared';
import {
BundleResponse,
parseBundleOptionsFromRequestUrl,
@@ -21,7 +21,7 @@ const serveBundle = (
response: ServerResponse,
): void => {
const bundleResponse = new BundleResponse(response, request.headers.accept);
- const currentId = getIdByOptions(bundleOptions);
+ const currentId = helpers.getIdByOptions(bundleOptions);
const bundleStatusChangeHandler: BundlerEventListener<
'build-status-change'
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..fea00129 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..b445129b 100644
--- a/packages/dev-server/lib/server/ReactNativeWebServer.ts
+++ b/packages/dev-server/lib/server/ReactNativeWebServer.ts
@@ -3,14 +3,15 @@ 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 { helpers, type BundleOptions } from '@react-native-esbuild/shared';
import {
- combineWithDefaultBundleOptions,
- type BundleOptions,
-} from '@react-native-esbuild/config';
-import { createSymbolicateMiddleware } from '../middlewares';
+ createHmrMiddlewareForWeb,
+ createSymbolicateMiddleware,
+} from '../middlewares';
import { logger } from '../shared';
import type { DevServerOptions } from '../types';
import { DevServer } from './DevServer';
@@ -27,7 +28,9 @@ export class ReactNativeWebServer extends DevServer {
bundleOptions?: Partial,
) {
super(devServerOptions);
- this.bundleOptions = combineWithDefaultBundleOptions(bundleOptions ?? {});
+ this.bundleOptions = helpers.combineWithDefaultBundleOptions(
+ bundleOptions ?? {},
+ );
}
private proxyHandler(
@@ -96,6 +99,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,6 +127,20 @@ export class ReactNativeWebServer extends DevServer {
});
});
+ 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();
+ }
+ });
+
await onPostSetup?.(bundler);
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..0c720f9d 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,11 +30,12 @@ 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;
}
/**
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/HmrTransformer.ts b/packages/hmr/lib/HmrTransformer.ts
new file mode 100644
index 00000000..4759299d
--- /dev/null
+++ b/packages/hmr/lib/HmrTransformer.ts
@@ -0,0 +1,226 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import { performance } from 'node:perf_hooks';
+import type { Metafile } from 'esbuild';
+import { getReloadByDevSettingsProxy } from '@react-native-esbuild/internal';
+import {
+ AsyncTransformPipeline,
+ swcPresets,
+} from '@react-native-esbuild/transformer';
+import type { TransformerContext } from '@react-native-esbuild/transformer';
+import { colors } from '@react-native-esbuild/shared';
+import { DependencyGraph, isExternal } from 'esbuild-dependency-graph';
+import { logger } from './shared';
+import {
+ HMR_REGISTER_FUNCTION,
+ HMR_UPDATE_FUNCTION,
+ REACT_REFRESH_REGISTER_FUNCTION,
+ REACT_REFRESH_GET_SIGNATURE_FUNCTION,
+} from './constants';
+import type { BundleUpdate, PipelineBuilderOptions } from './types';
+
+const DUMMY_ESBUILD_ARGS = {
+ namespace: '',
+ suffix: '',
+ pluginData: undefined,
+} as const;
+
+export class HmrTransformer {
+ private static boundaryIndex = 0;
+ private dependencyGraph?: DependencyGraph;
+ private stripRootRegex: RegExp;
+ private externalPatternRegex: RegExp;
+ private pipeline: AsyncTransformPipeline;
+
+ public static isBoundary(path: string): boolean {
+ // `runtime.js`: To avoid wrong HMR behavior in monorepo.
+ return !path.includes('/node_modules/') && !path.endsWith('runtime.js');
+ }
+
+ public static asBoundary(id: string, code: string): string {
+ const ident = `__hmr${HmrTransformer.boundaryIndex++}`;
+ return `var ${ident} = ${HMR_REGISTER_FUNCTION}(${JSON.stringify(id)});
+ ${code}
+ ${ident}.dispose(function () { });
+ ${ident}.accept(function (payload) {
+ global.__hmr.reactRefresh.performReactRefresh();
+ });`;
+ }
+
+ public static registerAsExternalModule(
+ id: string,
+ code: string,
+ identifier: string,
+ ): string {
+ return `${code}\nglobal.__modules.external(${JSON.stringify(
+ id,
+ )}, ${identifier});`;
+ }
+
+ constructor(
+ private context: Omit & {
+ externalPattern: string;
+ },
+ builderOptions: PipelineBuilderOptions,
+ ) {
+ const {
+ fullyTransformPackageNames = [],
+ stripFlowPackageNames = [],
+ additionalBabelRules = [],
+ additionalSwcRules = [],
+ } = builderOptions;
+ this.stripRootRegex = new RegExp(`^${context.root}/?`);
+ this.externalPatternRegex = new RegExp(context.externalPattern);
+ this.pipeline = new AsyncTransformPipeline.builder(context)
+ .setSwcPreset(
+ swcPresets.getReactNativeRuntimePreset({
+ experimental: {
+ hmr: {
+ runtime: true,
+ refreshReg: REACT_REFRESH_REGISTER_FUNCTION,
+ refreshSig: REACT_REFRESH_GET_SIGNATURE_FUNCTION,
+ },
+ },
+ }),
+ )
+ .setFullyTransformPackages(fullyTransformPackageNames)
+ .setStripFlowPackages(stripFlowPackageNames)
+ .setAdditionalBabelTransformRules(additionalBabelRules)
+ .setAdditionalSwcTransformRules(additionalSwcRules)
+ .build();
+ }
+
+ public initializeDependencyGraph(metafile: Metafile): void {
+ this.dependencyGraph = new DependencyGraph(metafile, this.context.entry);
+ }
+
+ public async getDelta(modulePath: string): Promise {
+ try {
+ performance.mark(`hmr:build:${this.context.id}`);
+ const transformResult = await this.transformRuntime(modulePath);
+ const { code, target, dependencies } = transformResult;
+ const { duration } = performance.measure(
+ 'hmr:build-duration',
+ `hmr:build:${this.context.id}`,
+ );
+ this.stripRoot(modulePath);
+ logger.info(
+ [
+ target,
+ colors.gray(`+ ${dependencies} module(s)`),
+ 'transformed in',
+ colors.cyan(`${Math.floor(duration)}ms`),
+ ].join(' '),
+ );
+ return {
+ id: '',
+ code: this.asFallbackBoundary(code),
+ path: modulePath,
+ fullyReload: false,
+ };
+ } catch (error) {
+ logger.error('unable to transform runtime modules', error as Error);
+ return {
+ id: '',
+ code: '',
+ path: modulePath,
+ fullyReload: true,
+ };
+ }
+ }
+
+ private stripRoot(path: string): string {
+ return path.replace(this.stripRootRegex, '');
+ }
+
+ private async transformRuntime(
+ modulePath: string,
+ ): Promise<{ code: string; target: string; dependencies: number }> {
+ const dependencyGraph = this.dependencyGraph;
+ if (!dependencyGraph) {
+ throw new Error('dependency graph is not initialized');
+ }
+
+ const strippedModulePath = this.stripRoot(modulePath);
+ const moduleId = dependencyGraph.getModuleId(strippedModulePath);
+ const reverseDependencyIds =
+ dependencyGraph.inverseDependenciesOf(moduleId);
+
+ const transformedCodes = await Promise.all(
+ [moduleId, ...reverseDependencyIds].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 importPaths = this.getActualImportPaths(module.imports);
+
+ const { code } = await this.pipeline.transform(
+ rawCode,
+ { ...DUMMY_ESBUILD_ARGS, path: modulePath },
+ { externalPattern: this.context.externalPattern, importPaths },
+ );
+
+ return this.asCallHmrUpdate(modulePath, code);
+ }),
+ );
+
+ return {
+ code: transformedCodes.join('\n'),
+ target: strippedModulePath,
+ dependencies: reverseDependencyIds.length,
+ };
+ }
+
+ private getActualImportPaths(
+ 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(this.context.root, curr.path).split(':');
+ const actualPath = splitted[splitted.length - 1];
+ if (curr.original && !prev[curr.original]) {
+ logger.debug(
+ `${colors.gray(`├─ ${this.stripRoot(curr.original)} ▸`)} ${
+ this.externalPatternRegex.test(curr.original)
+ ? colors.gray('')
+ : this.stripRoot(actualPath)
+ }`,
+ );
+ }
+ return {
+ ...prev,
+ ...(curr.original ? { [curr.original]: actualPath } : null),
+ };
+ },
+ {} as Record,
+ );
+
+ logger.debug(
+ colors.gray(`╰─ ${Object.keys(importPaths).length} import(s)`),
+ );
+
+ return importPaths;
+ }
+
+ private asCallHmrUpdate(id: string, code: string): string {
+ return `${HMR_UPDATE_FUNCTION}(${JSON.stringify(id)}, function () {
+ ${code}
+ });`;
+ }
+
+ private asFallbackBoundary(code: string): string {
+ return `try {
+ ${code}
+ } catch (error) {
+ console.error('[HMR] unable to accept', error);
+ ${getReloadByDevSettingsProxy()}
+ }`;
+ }
+}
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/index.ts b/packages/hmr/lib/index.ts
new file mode 100644
index 00000000..e1f8201e
--- /dev/null
+++ b/packages/hmr/lib/index.ts
@@ -0,0 +1,5 @@
+export { HmrTransformer } from './HmrTransformer';
+export * from './server';
+export * from './constants';
+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..cdb48564
--- /dev/null
+++ b/packages/hmr/lib/runtime/setup.ts
@@ -0,0 +1,102 @@
+import * as RefreshRuntime from 'react-refresh/runtime';
+
+type HotModuleReplacementAcceptCallback = (payload: {
+ id: HotModuleReplacementId;
+}) => 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<
+ HotModuleReplacementId,
+ HotModuleReplacementContext | undefined
+ > = {};
+ public locked = false;
+ public acceptCallbacks: HotModuleReplacementAcceptCallback[] = [];
+ public disposeCallbacks: HotModuleReplacementDisposeCallback[] = [];
+
+ constructor(public id: HotModuleReplacementId) {}
+
+ 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: HotModuleReplacementId) => {
+ const context = HotModuleReplacementContext.registry[id];
+ if (context) {
+ context.lock();
+ return context;
+ }
+ return (HotModuleReplacementContext.registry[id] =
+ new HotModuleReplacementContext(id));
+ },
+ update: (id: HotModuleReplacementId, evalUpdates: () => void) => {
+ const context = HotModuleReplacementContext.registry[id];
+ 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..0ae88b50
--- /dev/null
+++ b/packages/hmr/lib/server/HmrAppServer.ts
@@ -0,0 +1,54 @@
+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 {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- from ws data
+ 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..904b3160
--- /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..6c95b075
--- /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..7360b435
--- /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..9344734b
--- /dev/null
+++ b/packages/hmr/lib/types.ts
@@ -0,0 +1,109 @@
+import type {
+ register,
+ createSignatureFunctionForTransform,
+} from 'react-refresh';
+import type {
+ BabelTransformRule,
+ SwcTransformRule,
+} from '@react-native-esbuild/transformer';
+
+/* eslint-disable no-var -- allow */
+declare global {
+ type HotModuleReplacementId = string;
+
+ interface HotModuleReplacementRuntimeModule {
+ register: (id: HotModuleReplacementId) => void;
+ update: (id: HotModuleReplacementId, 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;
+}
+
+export interface PipelineBuilderOptions {
+ fullyTransformPackageNames?: string[];
+ stripFlowPackageNames?: string[];
+ additionalBabelRules?: BabelTransformRule[];
+ additionalSwcRules?: SwcTransformRule[];
+}
+
+/**
+ * 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: number[];
+ readonly modified: HmrModule[];
+ isInitialUpdate: boolean;
+ revisionId: string;
+}
+export interface HmrModule {
+ module: [number, string];
+ sourceMappingURL: string | null;
+ sourceURL: string | null;
+}
+
+export interface BundleUpdate {
+ id: string;
+ path: string;
+ code: string;
+ fullyReload: boolean;
+}
diff --git a/packages/config/package.json b/packages/hmr/package.json
similarity index 54%
rename from packages/config/package.json
rename to packages/hmr/package.json
index b6912dfe..de7acc74 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,16 @@
"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-native-esbuild/transformer": "workspace:*",
+ "esbuild-dependency-graph": "^0.2.1",
+ "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/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/presets.ts b/packages/internal/lib/presets.ts
index 5cdd1b05..e8af50d8 100644
--- a/packages/internal/lib/presets.ts
+++ b/packages/internal/lib/presets.ts
@@ -1,6 +1,6 @@
/* eslint-disable quotes -- Allow using backtick */
import fs from 'node:fs/promises';
-import type { BundleOptions } from '@react-native-esbuild/config';
+import type { BundleOptions } from '@react-native-esbuild/shared';
import { resolveFromRoot, wrapWithIIFE } from './helpers';
import type { Asset } from './types';
@@ -22,19 +22,13 @@ export const getInjectVariables = (dev: boolean): string[] => [
`global = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this`,
];
-const getReactNativePolyfills = (root: string): Promise => {
+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 Promise.all(
- getPolyfills().map((scriptPath) =>
- fs
- .readFile(scriptPath, { encoding: 'utf-8' })
- .then((code) => wrapWithIIFE(code, scriptPath)),
- ),
- );
+ return getPolyfills();
};
export const getReactNativeInitializeCore = (root: string): string => {
@@ -56,16 +50,24 @@ export const getPreludeScript = async (
{ dev = true }: BundleOptions,
root: string,
): Promise => {
- const polyfills = await getReactNativePolyfills(root);
+ 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))};`,
- ...polyfills,
+ ...scripts,
].join('\n');
return initialScripts;
};
+
/**
* Get asset registration script.
*
@@ -83,20 +85,18 @@ export const getAssetRegistrationScript = ({
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,
- },
- )});
- `;
+ 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,
+ },
+ )});`;
};
/**
diff --git a/packages/internal/lib/types.ts b/packages/internal/lib/types.ts
index d4bf6e48..a2c1ea9c 100644
--- a/packages/internal/lib/types.ts
+++ b/packages/internal/lib/types.ts
@@ -1,4 +1,4 @@
-import type { BundlerSupportPlatform } from '@react-native-esbuild/config';
+import type { BundlerSupportPlatform } from '@react-native-esbuild/shared';
export interface Asset {
// `/path/to/asset/image.png`
diff --git a/packages/internal/package.json b/packages/internal/package.json
index 82c5e887..7fce78d9 100644
--- a/packages/internal/package.json
+++ b/packages/internal/package.json
@@ -37,7 +37,7 @@
"url": "https://github.com/leegeunhyeok/react-native-esbuild/issues"
},
"devDependencies": {
- "@react-native-esbuild/config": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"esbuild": "^0.19.5"
},
"dependencies": {
diff --git a/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts b/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts
index 3a2dab34..1832b05b 100644
--- a/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/__tests__/assetRegisterPlugin.test.ts
@@ -4,9 +4,9 @@ import { faker } from '@faker-js/faker';
import type { OnLoadArgs } from 'esbuild';
import type { PluginContext } from '@react-native-esbuild/core';
import {
- SUPPORT_PLATFORMS,
+ constants,
type BundlerSupportPlatform,
-} from '@react-native-esbuild/config';
+} from '@react-native-esbuild/shared';
import type { AssetScale } from '@react-native-esbuild/internal';
import {
getAssetPriority,
@@ -44,7 +44,7 @@ describe('assetRegisterPlugin', () => {
filename = faker.string.alphanumeric(10);
extension = faker.helpers.arrayElement(['.png', '.jpg', '.jpeg', '.gif']);
scale = faker.number.int({ min: 1, max: 3 }) as AssetScale;
- platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
+ platform = faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS);
});
it('should platform and scale suffixed priority is 3', () => {
@@ -78,7 +78,7 @@ describe('assetRegisterPlugin', () => {
filename = faker.string.alphanumeric(10);
extension = faker.helpers.arrayElement(['.png', '.jpg', '.jpeg', '.gif']);
scale = faker.number.int({ min: 1, max: 3 }) as AssetScale;
- platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
+ platform = faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS);
});
describe('when non-suffixed filename is present', () => {
@@ -152,7 +152,7 @@ describe('assetRegisterPlugin', () => {
filename = faker.string.alphanumeric(10);
extension = faker.helpers.arrayElement(['.png', '.jpg', '.jpeg', '.gif']);
scale = faker.number.int({ min: 1, max: 3 }) as AssetScale;
- platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
+ platform = faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS);
});
describe('when platform suffixed basename is present', () => {
@@ -245,7 +245,7 @@ describe('assetRegisterPlugin', () => {
let suffixPathResult: SuffixPathResult;
beforeEach(() => {
- platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
+ platform = faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS);
suffixPathResult = getSuffixedPath(pullPath, { platform });
});
@@ -286,7 +286,7 @@ describe('assetRegisterPlugin', () => {
let suffixPathResult: SuffixPathResult;
beforeEach(() => {
- platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
+ platform = faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS);
scale = faker.number.int({ min: 1, max: 3 }) as AssetScale;
suffixPathResult = getSuffixedPath(pullPath, { platform, scale });
});
@@ -358,7 +358,7 @@ describe('assetRegisterPlugin', () => {
let platform: BundlerSupportPlatform;
beforeEach(() => {
- platform = faker.helpers.arrayElement(SUPPORT_PLATFORMS);
+ platform = faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS);
mockContext(platform);
mockArgs(platform);
mockFileSystem([
@@ -505,7 +505,7 @@ describe('assetRegisterPlugin', () => {
describe('when only non-suffixed assets are exist', () => {
beforeEach(() => {
- mockContext(faker.helpers.arrayElement(SUPPORT_PLATFORMS));
+ mockContext(faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS));
mockFileSystem([`${filename}${extension}`]);
});
@@ -518,7 +518,7 @@ describe('assetRegisterPlugin', () => {
describe('when only @1x suffixed asset is exist', () => {
beforeEach(() => {
- mockContext(faker.helpers.arrayElement(SUPPORT_PLATFORMS));
+ mockContext(faker.helpers.arrayElement(constants.SUPPORT_PLATFORMS));
mockFileSystem([`${filename}@1x${extension}`]);
});
diff --git a/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts b/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts
index b115bfda..ccb86f10 100644
--- a/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/assetRegisterPlugin.ts
@@ -6,6 +6,7 @@ import {
type Asset,
} from '@react-native-esbuild/internal';
import { ASSET_EXTENSIONS } from '@react-native-esbuild/internal';
+import { HmrTransformer } from '@react-native-esbuild/hmr';
import type { AssetRegisterPluginConfig, SuffixPathResult } from '../types';
import {
copyAssetsToDestination,
@@ -96,12 +97,19 @@ export const createAssetRegisterPlugin: ReactNativeEsbuildPluginCreator<
build.onLoad({ filter: /./, namespace: ASSET_NAMESPACE }, async (args) => {
const asset = await resolveScaledAssets(context, args);
+ const assetRegistrationScript = getAssetRegistrationScript(asset);
assets.push(asset);
return {
resolveDir: path.dirname(args.path),
- contents: getAssetRegistrationScript(asset),
+ contents: context.enableHmr
+ ? HmrTransformer.registerAsExternalModule(
+ args.path,
+ assetRegistrationScript,
+ 'module.exports',
+ )
+ : assetRegistrationScript,
loader: 'js',
};
});
diff --git a/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts b/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts
index cf129080..11c25f16 100644
--- a/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts
+++ b/packages/plugins/lib/assetRegisterPlugin/helpers/path.ts
@@ -8,13 +8,12 @@ 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,
+ constants,
type BundlerSupportPlatform,
-} from '@react-native-esbuild/config';
+} from '@react-native-esbuild/shared';
import type { SuffixPathResult } from '../../types';
-const PLATFORM_SUFFIX_PATTERN = SUPPORT_PLATFORMS.map(
+const PLATFORM_SUFFIX_PATTERN = constants.SUPPORT_PLATFORMS.map(
(platform) => `.${platform}`,
).join('|');
@@ -181,7 +180,10 @@ export const resolveScaledAssets = async (
return ALLOW_SCALES[context.platform]?.includes(scale) ?? true;
})
.sort(),
- httpServerLocation: path.join(ASSET_PATH, path.dirname(relativePath)),
+ httpServerLocation: path.join(
+ constants.ASSET_PATH,
+ path.dirname(relativePath),
+ ),
hash: md5(imageData),
dimensions: {
width: dimensions?.width ?? 0,
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..755febe4
--- /dev/null
+++ b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/helpers/json.ts
@@ -0,0 +1,31 @@
+import fs from 'node:fs/promises';
+import type { PluginBuild } from 'esbuild';
+import { HmrTransformer } from '@react-native-esbuild/hmr';
+
+/**
+ * When development mode & HMR enabled, the '.json' contents
+ * must be registered in the global module registry for HMR.
+ */
+export const jsonAsJsModule = (build: PluginBuild): void => {
+ build.onLoad({ filter: /\.json$/ }, async (args) => {
+ const rawJson = await fs.readFile(args.path, { encoding: 'utf-8' });
+ const parsedJson = JSON.parse(rawJson) as Record;
+ const identifier = 'json';
+
+ return {
+ contents: HmrTransformer.registerAsExternalModule(
+ args.path,
+ `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..b4ae4873 100644
--- a/packages/plugins/lib/reactNativeRuntimeTransformPlugin/reactNativeRuntimeTransformPlugin.ts
+++ b/packages/plugins/lib/reactNativeRuntimeTransformPlugin/reactNativeRuntimeTransformPlugin.ts
@@ -1,6 +1,5 @@
import fs from 'node:fs/promises';
import path from 'node:path';
-import type { OnLoadResult } from 'esbuild';
import {
ReactNativeEsbuildBundler as Bundler,
type ReactNativeEsbuildPluginCreator,
@@ -11,6 +10,11 @@ import {
swcPresets,
type AsyncTransformStep,
} from '@react-native-esbuild/transformer';
+import {
+ HmrTransformer,
+ REACT_REFRESH_REGISTER_FUNCTION,
+ REACT_REFRESH_GET_SIGNATURE_FUNCTION,
+} from '@react-native-esbuild/hmr';
import { logger } from '../shared';
import type { ReactNativeRuntimeTransformPluginConfig } from '../types';
import {
@@ -18,6 +22,7 @@ import {
getTransformedCodeFromFileSystemCache,
writeTransformedCodeToInMemoryCache,
writeTransformedCodeToFileSystemCache,
+ jsonAsJsModule,
} from './helpers';
const NAME = 'react-native-runtime-transform-plugin';
@@ -28,7 +33,6 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
name: NAME,
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 = [],
@@ -38,33 +42,38 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
const additionalBabelRules = additionalTransformRules?.babel ?? [];
const additionalSwcRules = additionalTransformRules?.swc ?? [];
const injectScriptPaths = [
- getReactNativeInitializeCore(context.root),
+ ...[
+ getReactNativeInitializeCore(context.root),
+ // `hmr/runtime` should import after `initializeCore` initialized.
+ context.enableHmr ? '@react-native-esbuild/hmr/runtime' : undefined,
+ ],
...(config?.injectScriptPaths ?? []),
- ];
-
- const reactNativeRuntimePreset = swcPresets.getReactNativeRuntimePreset(
- context.config.transformer?.jsc,
- );
+ ].filter(Boolean) as string[];
+
+ const reactNativeRuntimePreset = swcPresets.getReactNativeRuntimePreset({
+ experimental: {
+ hmr: context.enableHmr
+ ? {
+ runtime: false,
+ refreshReg: REACT_REFRESH_REGISTER_FUNCTION,
+ refreshSig: REACT_REFRESH_GET_SIGNATURE_FUNCTION,
+ }
+ : undefined,
+ },
+ });
const onBeforeTransform: AsyncTransformStep = async (
code,
- args,
+ _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
+ * 1. Use previous transformed result and skip transform
* when file is not changed and transform result exist in memory.
*/
const inMemoryCache = getTransformedCodeFromInMemoryCache(
@@ -75,12 +84,12 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
return { code: inMemoryCache, done: true };
}
- // 3. Transform code on each build task when cache is disabled.
+ // 2. Transform code on each build task when cache is disabled.
if (!cacheEnabled) {
return { code, done: false };
}
- // 4. Trying to get cache from file system.
+ // 3. Trying to get cache from file system.
// = cache exist ? use cache : transform code
const cachedCode = await getTransformedCodeFromFileSystemCache(
cacheController,
@@ -92,7 +101,7 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
const onAfterTransform: AsyncTransformStep = async (
code,
- _args,
+ args,
moduleMeta,
) => {
const cacheConfig = {
@@ -109,11 +118,16 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
);
}
- return { code, done: true };
+ return {
+ code:
+ context.enableHmr && HmrTransformer.isBoundary(args.path)
+ ? HmrTransformer.asBoundary(args.path, code)
+ : code,
+ done: true,
+ };
};
- let transformPipeline: AsyncTransformPipeline;
- const transformPipelineBuilder = new AsyncTransformPipeline.builder(context)
+ const transformPipeline = new AsyncTransformPipeline.builder(context)
.setSwcPreset(reactNativeRuntimePreset)
.setInjectScripts(injectScriptPaths)
.setFullyTransformPackages(fullyTransformPackageNames)
@@ -121,18 +135,21 @@ export const createReactNativeRuntimeTransformPlugin: ReactNativeEsbuildPluginCr
.setAdditionalBabelTransformRules(additionalBabelRules)
.setAdditionalSwcTransformRules(additionalSwcRules)
.onStart(onBeforeTransform)
- .onEnd(onAfterTransform);
+ .onEnd(onAfterTransform)
+ .build();
- build.onStart(() => {
- transformPipeline = transformPipelineBuilder.build();
- });
+ context.enableHmr && jsonAsJsModule(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,
+ contents: (
+ await transformPipeline.transform(rawCode, args, {
+ externalPattern: context.externalPattern,
+ })
+ ).code,
loader: 'js',
- } as OnLoadResult;
+ };
});
build.onEnd(async (args) => {
diff --git a/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts b/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts
index 71fb6d72..c5795bef 100644
--- a/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts
+++ b/packages/plugins/lib/reactNativeWebPlugin/reactNativeWebPlugin.ts
@@ -1,7 +1,7 @@
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 { helpers } from '@react-native-esbuild/shared';
import { logger } from '../shared';
import { generateIndexPage } from './helpers';
@@ -21,7 +21,9 @@ export const createReactNativeWebPlugin: ReactNativeEsbuildPluginCreator = (
const { root, platform, outfile, mode } = context;
const { template, placeholders } = context.config.web ?? {};
const destination =
- mode === 'watch' ? getDevServerPublicPath(root) : path.dirname(outfile);
+ mode === 'watch'
+ ? helpers.getDevServerPublicPath(root)
+ : path.dirname(outfile);
const bundleFilename = path.basename(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..8ebb001e 100644
--- a/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts
+++ b/packages/plugins/lib/svgTransformPlugin/svgTransformPlugin.ts
@@ -1,6 +1,8 @@
import fs from 'node:fs/promises';
import { transform } from '@svgr/core';
import type { ReactNativeEsbuildPluginCreator } from '@react-native-esbuild/core';
+import { HmrTransformer } from '@react-native-esbuild/hmr';
+import { defaultTemplate, SVG_COMPONENT_NAME } from './templates';
const NAME = 'svg-transform-plugin';
@@ -13,15 +15,24 @@ export const createSvgTransformPlugin: ReactNativeEsbuildPluginCreator = (
build.onLoad({ filter: /\.svg$/ }, async (args) => {
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: context.enableHmr
+ ? HmrTransformer.registerAsExternalModule(
+ args.path,
+ 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..733296e2
--- /dev/null
+++ b/packages/plugins/lib/svgTransformPlugin/templates/index.ts
@@ -0,0 +1,16 @@
+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..3ae067ed 100644
--- a/packages/plugins/lib/types.ts
+++ b/packages/plugins/lib/types.ts
@@ -1,4 +1,4 @@
-import type { BundlerSupportPlatform } from '@react-native-esbuild/config';
+import type { BundlerSupportPlatform } from '@react-native-esbuild/shared';
// asset-register-plugin
export interface AssetRegisterPluginConfig {
diff --git a/packages/plugins/package.json b/packages/plugins/package.json
index aac9060c..58088fb7 100644
--- a/packages/plugins/package.json
+++ b/packages/plugins/package.json
@@ -44,11 +44,11 @@
"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",
"image-size": "^1.0.2",
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/config/lib/shares.ts b/packages/shared/lib/constants.ts
similarity index 71%
rename from packages/config/lib/shares.ts
rename to packages/shared/lib/constants.ts
index 8cbe1b2f..738fd961 100644
--- a/packages/config/lib/shares.ts
+++ b/packages/shared/lib/constants.ts
@@ -6,3 +6,7 @@ export const GLOBAL_CACHE_DIR = 'react-native-esbuild';
export const LOCAL_CACHE_DIR = '.rne';
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..6a34fe52
--- /dev/null
+++ b/packages/shared/lib/enums.ts
@@ -0,0 +1,11 @@
+/**
+ * Flags for `BundleOptions`
+ */
+export enum OptionFlag {
+ None = 0b00000000,
+ PlatformAndroid = 0b00000001,
+ PlatformIos = 0b00000010,
+ PlatformWeb = 0b00000100,
+ Dev = 0b00001000,
+ Minify = 0b00010000,
+}
diff --git a/packages/config/lib/common/core.ts b/packages/shared/lib/helpers.ts
similarity index 90%
rename from packages/config/lib/common/core.ts
rename to packages/shared/lib/helpers.ts
index bc24da2c..1baf87dc 100644
--- a/packages/config/lib/common/core.ts
+++ b/packages/shared/lib/helpers.ts
@@ -1,10 +1,13 @@
import path from 'node:path';
+import { OptionFlag } from './enums';
+import type { BundleOptions } from './types';
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,
@@ -62,10 +65,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..df22984e
--- /dev/null
+++ b/packages/shared/lib/index.ts
@@ -0,0 +1,13 @@
+import colors from 'colors';
+import { isCI, isTTY } from './utils';
+
+(isCI() || !isTTY()) && colors.disable();
+
+export * as colors from 'colors';
+export * as constants from './constants';
+export * as enums from './enums';
+export * as helpers from './helpers';
+export * as utils from './utils';
+export { Logger, LogLevel } from './logger';
+
+export type * from './types';
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/config/lib/types.ts b/packages/shared/lib/types.ts
similarity index 55%
rename from packages/config/lib/types.ts
rename to packages/shared/lib/types.ts
index 52d4f1c3..21fa4d80 100644
--- a/packages/config/lib/types.ts
+++ b/packages/shared/lib/types.ts
@@ -10,15 +10,3 @@ export interface BundleOptions {
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/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 90%
rename from packages/utils/package.json
rename to packages/shared/package.json
index a3092323..2e156982 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"
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/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/helpers/transformer.ts b/packages/transformer/lib/helpers/transformer.ts
index 4e4877b3..daf06066 100644
--- a/packages/transformer/lib/helpers/transformer.ts
+++ b/packages/transformer/lib/helpers/transformer.ts
@@ -27,7 +27,10 @@ export const transformByBabelRule = (
context: TransformerContext,
): 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);
};
@@ -37,11 +40,10 @@ export const transformSyncByBabelRule = (
context: TransformerContext,
): string | null => {
return rule.test(context.path, code)
- ? transformSyncWithBabel(
- code,
+ ? transformSyncWithBabel(code, {
context,
- ruleOptionsToPreset(rule.options, code),
- )
+ preset: ruleOptionsToPreset(rule.options, code),
+ })
: null;
};
@@ -51,7 +53,10 @@ export const transformBySwcRule = (
context: TransformerContext,
): 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);
};
@@ -61,10 +66,9 @@ export const transformSyncBySwcRule = (
context: TransformerContext,
): 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/pipelines/AsyncTransformPipeline.ts b/packages/transformer/lib/pipelines/AsyncTransformPipeline.ts
index e3330a0c..f9002b99 100644
--- a/packages/transformer/lib/pipelines/AsyncTransformPipeline.ts
+++ b/packages/transformer/lib/pipelines/AsyncTransformPipeline.ts
@@ -8,8 +8,8 @@ import {
} from '../transformer';
import { transformByBabelRule, transformBySwcRule } from '../helpers';
import type { AsyncTransformStep, ModuleMeta, TransformResult } from '../types';
-import { TransformPipeline } from './pipeline';
-import { TransformPipelineBuilder } from './builder';
+import { TransformPipeline } from './TransformPipeline';
+import { TransformPipelineBuilder } from './TransformPipelineBuilder';
export class AsyncTransformPipelineBuilder extends TransformPipelineBuilder<
AsyncTransformStep,
@@ -40,14 +40,14 @@ export class AsyncTransformPipelineBuilder extends TransformPipelineBuilder<
this.fullyTransformPackageNames,
);
if (fullyTransformPackagesRegExp) {
- pipeline.addStep(async (code, args) => {
+ pipeline.addStep(async (code, args, moduleMeta) => {
if (fullyTransformPackagesRegExp.test(args.path)) {
return {
- code: await transformWithBabel(
- code,
- this.getContext(args),
- this.presets.babelFullyTransform,
- ),
+ code: await transformWithBabel(code, {
+ moduleMeta,
+ context: this.getContext(args),
+ preset: this.presets.babelFullyTransform,
+ }),
// skip other transformations when fully transformed
done: true,
};
@@ -61,12 +61,15 @@ export class AsyncTransformPipelineBuilder extends TransformPipelineBuilder<
this.stripFlowPackageNames,
);
if (stripFlowPackageNamesRegExp) {
- pipeline.addStep((code, args) => {
+ pipeline.addStep((code, args, moduleMeta) => {
if (
stripFlowPackageNamesRegExp.test(args.path) ||
this.isFlow(code, args.path)
) {
- code = stripFlowWithSucrase(code, this.getContext(args));
+ code = stripFlowWithSucrase(code, {
+ moduleMeta,
+ context: this.getContext(args),
+ });
}
return Promise.resolve({ code, done: false });
@@ -96,13 +99,13 @@ export class AsyncTransformPipelineBuilder extends TransformPipelineBuilder<
}
// 6. Transform code to es5.
- pipeline.addStep(async (code, args) => {
+ pipeline.addStep(async (code, args, moduleMeta) => {
return {
- code: await transformWithSwc(
- code,
- this.getContext(args),
- this.swcPreset,
- ),
+ code: await transformWithSwc(code, {
+ moduleMeta,
+ context: this.getContext(args),
+ preset: this.swcPreset,
+ }),
done: true,
};
});
@@ -117,9 +120,14 @@ export class AsyncTransformPipeline extends TransformPipeline {
+ async transform(
+ code: string,
+ args: OnLoadArgs,
+ baseModuleMeta?: Pick,
+ ): Promise {
const fileStat = await fs.stat(args.path);
const moduleMeta: ModuleMeta = {
+ ...baseModuleMeta,
stats: fileStat,
hash: this.getHash(this.context.id, args.path, fileStat.mtimeMs),
};
diff --git a/packages/transformer/lib/pipelines/SyncTransformPipeline.ts b/packages/transformer/lib/pipelines/SyncTransformPipeline.ts
index 8eef2f51..edae191c 100644
--- a/packages/transformer/lib/pipelines/SyncTransformPipeline.ts
+++ b/packages/transformer/lib/pipelines/SyncTransformPipeline.ts
@@ -8,8 +8,8 @@ import {
} from '../transformer';
import { transformSyncByBabelRule, transformSyncBySwcRule } from '../helpers';
import type { SyncTransformStep, TransformResult, ModuleMeta } from '../types';
-import { TransformPipeline } from './pipeline';
-import { TransformPipelineBuilder } from './builder';
+import { TransformPipeline } from './TransformPipeline';
+import { TransformPipelineBuilder } from './TransformPipelineBuilder';
export class SyncTransformPipelineBuilder extends TransformPipelineBuilder<
SyncTransformStep,
@@ -40,14 +40,14 @@ export class SyncTransformPipelineBuilder extends TransformPipelineBuilder<
this.fullyTransformPackageNames,
);
if (fullyTransformPackagesRegExp) {
- pipeline.addStep((code, args) => {
+ pipeline.addStep((code, args, moduleMeta) => {
if (fullyTransformPackagesRegExp.test(args.path)) {
return {
- code: transformSyncWithBabel(
- code,
- this.getContext(args),
- this.presets.babelFullyTransform,
- ),
+ code: transformSyncWithBabel(code, {
+ moduleMeta,
+ context: this.getContext(args),
+ preset: this.presets.babelFullyTransform,
+ }),
// skip other transformations when fully transformed
done: true,
};
@@ -61,12 +61,15 @@ export class SyncTransformPipelineBuilder extends TransformPipelineBuilder<
this.stripFlowPackageNames,
);
if (stripFlowPackageNamesRegExp) {
- pipeline.addStep((code, args) => {
+ pipeline.addStep((code, args, moduleMeta) => {
if (
stripFlowPackageNamesRegExp.test(args.path) ||
this.isFlow(code, args.path)
) {
- code = stripFlowWithSucrase(code, this.getContext(args));
+ code = stripFlowWithSucrase(code, {
+ moduleMeta,
+ context: this.getContext(args),
+ });
}
return { code, done: false };
@@ -96,9 +99,13 @@ export class SyncTransformPipelineBuilder extends TransformPipelineBuilder<
}
// 6. Transform code to es5.
- pipeline.addStep((code, args) => {
+ pipeline.addStep((code, args, moduleMeta) => {
return {
- code: transformSyncWithSwc(code, this.getContext(args), this.swcPreset),
+ code: transformSyncWithSwc(code, {
+ moduleMeta,
+ context: this.getContext(args),
+ preset: this.swcPreset,
+ }),
done: true,
};
});
@@ -128,9 +135,14 @@ export class SyncTransformPipeline extends TransformPipeline
return this;
}
- transform(code: string, args: OnLoadArgs): TransformResult {
+ transform(
+ code: string,
+ args: OnLoadArgs,
+ baseModuleMeta?: Pick,
+ ): TransformResult {
const fileStat = fs.statSync(args.path);
const moduleMeta: ModuleMeta = {
+ ...baseModuleMeta,
stats: fileStat,
hash: this.getHash(this.context.id, args.path, fileStat.mtimeMs),
};
diff --git a/packages/transformer/lib/pipelines/pipeline.ts b/packages/transformer/lib/pipelines/TransformPipeline.ts
similarity index 82%
rename from packages/transformer/lib/pipelines/pipeline.ts
rename to packages/transformer/lib/pipelines/TransformPipeline.ts
index 84c84c3e..034ddec1 100644
--- a/packages/transformer/lib/pipelines/pipeline.ts
+++ b/packages/transformer/lib/pipelines/TransformPipeline.ts
@@ -1,6 +1,6 @@
import type { OnLoadArgs } from 'esbuild';
import md5 from 'md5';
-import type { TransformStep, TransformerContext } from '../types';
+import type { ModuleMeta, TransformStep, TransformerContext } from '../types';
export abstract class TransformPipeline> {
protected steps: Step[] = [];
@@ -36,5 +36,9 @@ export abstract class TransformPipeline> {
return this;
}
- abstract transform(code: string, args: OnLoadArgs): ReturnType;
+ abstract transform(
+ code: string,
+ args: OnLoadArgs,
+ baseModuleMeta?: Pick,
+ ): ReturnType;
}
diff --git a/packages/transformer/lib/pipelines/builder.ts b/packages/transformer/lib/pipelines/TransformPipelineBuilder.ts
similarity index 97%
rename from packages/transformer/lib/pipelines/builder.ts
rename to packages/transformer/lib/pipelines/TransformPipelineBuilder.ts
index 366f9e28..c4374971 100644
--- a/packages/transformer/lib/pipelines/builder.ts
+++ b/packages/transformer/lib/pipelines/TransformPipelineBuilder.ts
@@ -8,7 +8,7 @@ import type {
TransformerOptionsPreset,
} from '../types';
import { babelPresets } from '../transformer';
-import type { TransformPipeline } from './pipeline';
+import type { TransformPipeline } from './TransformPipeline';
const FLOW_SYMBOL = ['@flow', '@noflow'] as const;
diff --git a/packages/transformer/lib/transformer/babel/babel.ts b/packages/transformer/lib/transformer/babel/babel.ts
index 95261d9c..a03c13ed 100644
--- a/packages/transformer/lib/transformer/babel/babel.ts
+++ b/packages/transformer/lib/transformer/babel/babel.ts
@@ -19,10 +19,9 @@ const loadBabelOptions = (
export const transformWithBabel: AsyncTransformer = async (
code: string,
- context,
- preset,
+ { context, moduleMeta, preset },
) => {
- const babelOptions = loadBabelOptions(context, preset?.(context));
+ const babelOptions = loadBabelOptions(context, preset?.(context, moduleMeta));
if (!babelOptions) {
throw new Error('cannot load babel options');
}
@@ -37,10 +36,9 @@ export const transformWithBabel: AsyncTransformer = async (
export const transformSyncWithBabel: SyncTransformer = (
code: string,
- context,
- preset,
+ { context, moduleMeta, preset },
) => {
- const babelOptions = loadBabelOptions(context, preset?.(context));
+ const babelOptions = loadBabelOptions(context, preset?.(context, moduleMeta));
if (!babelOptions) {
throw new Error('cannot load babel options');
}
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..8978c00f 100644
--- a/packages/transformer/lib/transformer/swc/presets.ts
+++ b/packages/transformer/lib/transformer/swc/presets.ts
@@ -6,9 +6,22 @@ import type {
} from '@swc/core';
import type {
TransformerOptionsPreset,
+ ReactNativeRuntimePresetOptions,
SwcJestPresetOptions,
+ ModuleMeta,
+ SwcMinifyPresetOptions,
+ TransformerContext,
} from '../../types';
+/**
+ * TODO: move into hmr package.
+ *
+ * @see `HmrTransformer.isBoundary`
+ */
+const isHMRBoundary = (path: string): boolean => {
+ return !path.includes('/node_modules/') && !path.endsWith('runtime.js');
+};
+
const getParserOptions = (path: string): TsParserConfig | EsParserConfig => {
return /\.tsx?$/.test(path)
? ({
@@ -23,13 +36,35 @@ const getParserOptions = (path: string): TsParserConfig | EsParserConfig => {
} as EsParserConfig);
};
+const getSwcExperimental = (
+ context: TransformerContext,
+ moduleMeta?: ModuleMeta,
+ options?: ReactNativeRuntimePresetOptions,
+): JscConfig['experimental'] => {
+ if (options?.experimental?.hmr && isHMRBoundary(context.path)) {
+ return {
+ plugins: [
+ [
+ 'swc-plugin-global-module',
+ {
+ runtimeModule: options.experimental.hmr.runtime,
+ externalPattern: moduleMeta?.externalPattern,
+ importPaths: moduleMeta?.importPaths,
+ },
+ ],
+ ],
+ };
+ }
+ return undefined;
+};
+
/**
* swc transform options preset for react-native runtime.
*/
const getReactNativeRuntimePreset = (
- jscConfig?: Pick,
+ options?: ReactNativeRuntimePresetOptions,
): TransformerOptionsPreset => {
- return (context) => ({
+ return (context, moduleMeta) => ({
minify: false,
sourceMaps: false,
isModule: true,
@@ -39,10 +74,22 @@ const getReactNativeRuntimePreset = (
parser: getParserOptions(context.path),
target: 'es5',
loose: false,
- externalHelpers: true,
+ externalHelpers: !context.dev,
keepClassNames: true,
- transform: jscConfig?.transform,
- experimental: jscConfig?.experimental,
+ transform: {
+ react: {
+ development: context.dev,
+ // @ts-expect-error -- wrong type definition.
+ refresh:
+ options?.experimental?.hmr && isHMRBoundary(context.path)
+ ? {
+ refreshReg: options.experimental.hmr.refreshReg,
+ refreshSig: options.experimental.hmr.refreshSig,
+ }
+ : undefined,
+ },
+ },
+ experimental: getSwcExperimental(context, moduleMeta, options),
},
filename: context.path,
root: context.root,
@@ -95,11 +142,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..eb512f8c 100644
--- a/packages/transformer/lib/transformer/swc/swc.ts
+++ b/packages/transformer/lib/transformer/swc/swc.ts
@@ -1,18 +1,14 @@
-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, moduleMeta, preset },
) => {
- const { code: transformedCode } = await transform(code, preset?.(context));
+ const { code: transformedCode } = await transform(
+ code,
+ preset?.(context, moduleMeta),
+ );
if (typeof transformedCode !== 'string') {
throw new Error('swc transformed source is empty');
@@ -23,10 +19,12 @@ export const transformWithSwc: AsyncTransformer = async (
export const transformSyncWithSwc: SyncTransformer = (
code,
- context,
- preset,
+ { context, moduleMeta, preset },
) => {
- const { code: transformedCode } = transformSync(code, preset?.(context));
+ const { code: transformedCode } = transformSync(
+ code,
+ preset?.(context, moduleMeta),
+ );
if (typeof transformedCode !== 'string') {
throw new Error('swc transformed source is empty');
@@ -34,17 +32,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..6ea2dc90 100644
--- a/packages/transformer/lib/types.ts
+++ b/packages/transformer/lib/types.ts
@@ -5,16 +5,20 @@ import type { Options as SwcTransformOptions } from '@swc/core';
export type AsyncTransformer = (
code: string,
- context: TransformerContext,
- preset?: TransformerOptionsPreset,
+ config: TransformerConfig,
) => Promise;
export type SyncTransformer = (
code: string,
- context: TransformerContext,
- preset?: TransformerOptionsPreset,
+ config: TransformerConfig,
) => string;
+interface TransformerConfig {
+ context: TransformerContext;
+ moduleMeta?: ModuleMeta;
+ preset?: TransformerOptionsPreset;
+}
+
export interface TransformerContext {
id: number;
root: string;
@@ -25,11 +29,30 @@ export interface TransformerContext {
export type TransformerOptionsPreset = (
context: TransformerContext,
+ moduleMeta?: ModuleMeta,
) => TransformerOptions;
-// swc preset options
-export interface SwcReactNativeRuntimePresetOptions {
- reactRefresh?: { moduleId: string };
+export interface ReactNativeRuntimePresetOptions {
+ /**
+ * Options for experimental features.
+ */
+ experimental?: {
+ /**
+ * HMR(Hot Module Replacement) options.
+ *
+ * If `undefined`, HMR will be disabled.
+ */
+ hmr?: {
+ /**
+ * `runtimeModule` option in `swc-plugin-global-module`.
+ *
+ * @see github {@link https://github.com/leegeunhyeok/swc-plugin-global-module}
+ */
+ runtime: boolean;
+ refreshReg: string;
+ refreshSig: string;
+ };
+ };
}
export interface SwcJestPresetOptions {
@@ -63,6 +86,10 @@ export interface SwcJestPresetOptions {
};
}
+export interface SwcMinifyPresetOptions {
+ minify: boolean;
+}
+
export interface TransformRuleBase {
/**
* Predicator for transform
@@ -95,4 +122,6 @@ export interface TransformResult {
export interface ModuleMeta {
hash: string;
stats: Stats;
+ externalPattern?: string;
+ importPaths?: Record;
}
diff --git a/packages/transformer/package.json b/packages/transformer/package.json
index 4c10fcb3..49690d9e 100644
--- a/packages/transformer/package.json
+++ b/packages/transformer/package.json
@@ -43,12 +43,13 @@
},
"dependencies": {
"@babel/core": "^7.23.2",
- "@react-native-esbuild/config": "workspace:*",
+ "@react-native-esbuild/shared": "workspace:*",
"@swc/core": "^1.3.95",
"@swc/helpers": "^0.5.3",
"md5": "^2.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..551fe563 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3625,7 +3625,7 @@ __metadata:
"@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
@@ -3650,17 +3650,20 @@ __metadata:
"@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/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-plugin-module-id: ^0.1.3
invariant: ^2.2.4
ora: ^5.4.1
peerDependencies:
react-native: "*"
+ swc-plugin-global-module: "*"
languageName: unknown
linkType: soft
@@ -3672,9 +3675,10 @@ __metadata:
"@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,6 +3706,22 @@ __metadata:
languageName: unknown
linkType: soft
+"@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:*"
+ "@react-native-esbuild/transformer": "workspace:*"
+ "@swc/helpers": ^0.5.2
+ "@types/react-refresh": ^0.14.3
+ esbuild: ^0.19.3
+ esbuild-dependency-graph: ^0.2.1
+ react-refresh: ^0.14.0
+ ws: ^8.14.2
+ languageName: unknown
+ linkType: soft
+
"@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"
@@ -3736,9 +3756,10 @@ __metadata:
"@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
@@ -3751,6 +3772,18 @@ __metadata:
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:
+ "@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-esbuild/symbolicate@workspace:*, @react-native-esbuild/symbolicate@workspace:packages/symbolicate":
version: 0.0.0-use.local
resolution: "@react-native-esbuild/symbolicate@workspace:packages/symbolicate"
@@ -3774,22 +3807,11 @@ __metadata:
md5: ^2.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 +4436,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 +4563,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:
@@ -4840,6 +4862,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 +8233,21 @@ __metadata:
languageName: node
linkType: hard
-"esbuild@npm:^0.19.5":
+"esbuild-dependency-graph@npm:^0.2.1":
+ version: 0.2.1
+ resolution: "esbuild-dependency-graph@npm:0.2.1"
+ checksum: b2de0328dd75dc509deafb927fdb106eea91a5e6dab655d4d911d6a525f55277754c9d4420a82517de562b8a63c30c22eaf4d81f66d36570f339d0a5d9adb769
+ 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 +17740,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 +19175,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: