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: