diff --git a/.changeset/good-jobs-serve.md b/.changeset/good-jobs-serve.md new file mode 100644 index 0000000000..e6ac1ed125 --- /dev/null +++ b/.changeset/good-jobs-serve.md @@ -0,0 +1,6 @@ +--- +"@rnx-kit/types-metro-serializer-esbuild": patch +"@rnx-kit/metro-serializer-esbuild": patch +--- + +Add new ways of configuring metro-serializer-esbuild for both serialization and transformation diff --git a/.vscode/settings.json b/.vscode/settings.json index 1da42fe8ef..f0b26819c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "editor.trimAutoWhitespace": true, "editor.insertSpaces": true, "editor.tabSize": 2, + "editor.defaultFormatter": "oxc.oxc-vscode", "eslint.enable": true, "eslint.workingDirectories": [{ "mode": "auto" }], // infer working directory based on .eslintrc/package.json location @@ -44,21 +45,6 @@ "**/lib-commonjs": true, "**/dist": true }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, "[handlebars]": { "editor.formatOnSave": false }, diff --git a/packages/metro-serializer-esbuild/README.md b/packages/metro-serializer-esbuild/README.md index 9febbbc5aa..54061197a4 100644 --- a/packages/metro-serializer-esbuild/README.md +++ b/packages/metro-serializer-esbuild/README.md @@ -75,6 +75,26 @@ make any changes. Next, configure Metro to use the esbuild serializer by making the following changes to `metro.config.js`: +```diff + const { makeMetroConfig } = require("@rnx-kit/metro-config"); ++const { MetroEsbuildFactory } = require("@rnx-kit/metro-serializer-esbuild"); ++const { makeSerializer, makeTransformer } = MetroEsbuildFactory(); + + module.exports = makeMetroConfig({ + serializer: { ++ customSerializer: makeSerializer() + }, ++ transformer: makeTransformer({ + // standard transformer options if needed, can be omitted + babelTransformerPath: require.resolve('react-native-svg-transformer') ++ }) + }); +``` + +> This will automatically configure the transformer options for development and production. + +Alternatively the legacy pattern can still be used as follows: + ```diff const { makeMetroConfig } = require("@rnx-kit/metro-config"); +const { diff --git a/packages/metro-serializer-esbuild/metro.transform.config.js b/packages/metro-serializer-esbuild/metro.transform.config.js new file mode 100644 index 0000000000..7506b4731b --- /dev/null +++ b/packages/metro-serializer-esbuild/metro.transform.config.js @@ -0,0 +1,60 @@ +const { exclusionList, makeMetroConfig } = require("@rnx-kit/metro-config"); +const path = require("node:path"); +const { MetroEsbuildFactory } = require("."); + +// Metro will pick up mocks if we don't exclude them +const blockList = exclusionList([/[/\\]__fixtures__[/\\].*[/\\]package.json$/]); + +const { makeSerializer, makeTransformer } = MetroEsbuildFactory({ + minify: false, + sourcemap: false, + transformWithEsbuild: true, +}); + +// We can't install dependencies for our test fixtures so we need to resolve +// them here to help Metro find them. +const extraNodeModules = (() => { + const fluentUtils = require + .resolve("@fluentui/utilities") + .replace("lib-commonjs", "lib"); + const fromFluentUtils = { paths: [fluentUtils] }; + const resolveFromFluent = (name) => + require.resolve(name, fromFluentUtils).replace("lib-commonjs", "lib"); + + const fromMetro = { paths: [require.resolve("metro/package.json")] }; + + return { + "@fluentui/dom-utilities": resolveFromFluent("@fluentui/dom-utilities"), + "@fluentui/merge-styles": resolveFromFluent("@fluentui/merge-styles"), + "@fluentui/set-version": resolveFromFluent("@fluentui/set-version"), + "@fluentui/utilities": fluentUtils, + "lodash-es": require.resolve("lodash-es"), + "metro-runtime/src/modules/asyncRequire.js": require.resolve( + "metro-runtime/src/modules/asyncRequire.js", + fromMetro + ), + "metro-runtime/src/polyfills/require.js": require.resolve( + "metro-runtime/src/polyfills/require.js", + fromMetro + ), + react: require.resolve("react"), + tslib: require.resolve("tslib", fromFluentUtils), + }; +})(); + +module.exports = makeMetroConfig({ + cacheStores: [], // Avoids issues with deleting the cache on Windows + reporter: { update: () => null }, + resolver: { + resolverMainFields: ["react-native", "module", "browser", "main"], + extraNodeModules, + blacklistRE: blockList, + blockList, + }, + serializer: { + customSerializer: makeSerializer([]), + getPolyfills: () => [], + }, + transformer: makeTransformer(), + watchFolders: Object.values(extraNodeModules).map((dir) => path.dirname(dir)), +}); diff --git a/packages/metro-serializer-esbuild/package.json b/packages/metro-serializer-esbuild/package.json index 78ab776234..9e467456d0 100644 --- a/packages/metro-serializer-esbuild/package.json +++ b/packages/metro-serializer-esbuild/package.json @@ -47,6 +47,7 @@ "@fluentui/utilities": "8.13.9", "@react-native-community/cli-types": "^20.0.0", "@react-native/babel-preset": "^0.83.0", + "@react-native/metro-babel-transformer": "^0.83.0", "@react-native/metro-config": "^0.83.0", "@rnx-kit/babel-plugin-import-path-remapper": "*", "@rnx-kit/babel-preset-metro-react-native": "*", @@ -58,6 +59,7 @@ "@types/node": "^24.0.0", "lodash-es": "^4.17.21", "metro": "^0.83.3", + "metro-babel-transformer": "^0.83.0", "metro-config": "^0.83.3", "metro-transform-worker": "^0.83.1", "react": "19.2.0", diff --git a/packages/metro-serializer-esbuild/src/esbuildTransformer.ts b/packages/metro-serializer-esbuild/src/esbuildTransformer.ts new file mode 100644 index 0000000000..747177ca09 --- /dev/null +++ b/packages/metro-serializer-esbuild/src/esbuildTransformer.ts @@ -0,0 +1,163 @@ +import type { Loader } from "esbuild"; +import { transform as esbuildTransform } from "esbuild"; +import type { + BabelTransformerArgs, + BabelTransformer, +} from "metro-babel-transformer"; +import type { MinifierResult } from "metro-transform-worker"; +import { createHash } from "node:crypto"; +import fs from "node:fs"; +import { getDefine, getEsbuildTransformOptions, getSupported } from "./options"; +import { patchSourceMapFilename } from "./sourceMap"; +import { inferBuildTarget } from "./targets"; + +// marker of whether the cache key is valid. If we are using a non-standard transformer the key will change on the +// subsequent call to reflect the new upstream. To get this to happen this is used as a signal value +let cacheKeyValid = false; + +const upstreamTransformer = (() => { + let upstreamPath = "@react-native/metro-babel-transformer"; + let upstream: BabelTransformer = require(upstreamPath); + + return (babelPath?: string) => { + if (babelPath && babelPath !== upstreamPath) { + upstreamPath = babelPath; + upstream = require(upstreamPath); + cacheKeyValid = false; + } + return upstream; + }; +})(); + +export const getCacheKey = (() => { + let cacheKey: string | null = null; + return () => { + if (!cacheKeyValid || !cacheKey) { + const upstream = upstreamTransformer(); + cacheKey = createHash("sha1") + .update(upstream.getCacheKey?.() ?? "upstream-unknown") + .update(fs.readFileSync(__filename, { encoding: "utf-8" })) + .update(require("esbuild/package.json").version) + .digest("hex"); + } + return cacheKey; + }; +})(); + +// Signal to @rnx-kit/babel-preset-metro-react-native that the esbuild +// transformer is active, so it can auto-select the "esbuild-transformer" +// profile and disable redundant babel plugins. +process.env["RNX_METRO_TRANSFORMER_ESBUILD"] = "1"; + +type WithMap = T & { map?: MinifierResult["map"] }; + +/** + * Determine the appropriate esbuild loader for a given filename. + * + * Uses `"jsx"` as the default for `.js` files because many React Native + * ecosystem JS files contain JSX syntax. esbuild's `"jsx"` loader is a + * superset of `"js"` so this is safe for plain JS files too. + */ +function getLoader(filename: string): Loader | null { + if (/\.[mc]?tsx?$/.test(filename)) { + return filename.endsWith("x") ? "tsx" : "ts"; + } else if (/\.[mc]?jsx?$/.test(filename)) { + return filename.endsWith("x") ? "jsx" : "js"; + } + return null; +} + +/** + * Metro transformer that uses esbuild as a first pass to strip TypeScript + * and optionally transform JSX, then delegates to + * `@react-native/metro-babel-transformer` for the full babel preset pipeline. + * + * Pipeline: + * 1. esbuild.transform() — TS stripping, optional JSX, preserves ESM imports + * 2. @react-native/metro-babel-transformer — babel preset, HMR, codegen, etc. + * + * TypeScript filenames are renamed to `.js` when passed to the upstream + * transformer to prevent redundant TS parsing by babel. + */ +export async function transform({ + filename, + src, + options, + plugins, +}: BabelTransformerArgs) { + const esbuildOptions = getEsbuildTransformOptions( + options.customTransformOptions + ); + + const { + babelTransformerPath, + jsx = "automatic", + jsxFactory, + jsxFragment, + jsxImportSource = "react", + target = inferBuildTarget(), + } = esbuildOptions; + + // parse the file to get the loader, if non-null esbuild will be used + const loader = getLoader(filename); + + // we will use a different filename for babel if we are mapping from TS to JS to avoid the @react-native/metro-babel-transformer + // going down the slow codepath of trying to parse TS syntax and bypassing the hermes-parser. + const transformedFilename = + loader === "tsx" || loader === "ts" + ? filename.replace(/\.[mc]?tsx?$/, ".js") + : filename; + + if (loader) { + const jsxDev = + esbuildOptions.jsxDev ?? (jsx === "automatic" && options.dev); + + const esbuildResult = await esbuildTransform(src, { + sourcefile: filename, + loader, + // don't downlevel here except for what is listed in the supported settings + target: "esnext", + supported: getSupported(target), + jsx, + jsxFactory, + jsxFragment, + jsxImportSource, + jsxDev, + define: getDefine(options), + // inline sourcemaps, babel will decode and return it as a separate map object + sourcemap: "inline", + // turn off sources content to save memory, babel doesn't use it and metro doesn't support it + sourcesContent: false, + // don't minify at this stage + minify: false, + }); + + // remember the transformed source to pass to babel + src = esbuildResult.code; + } + + // Delegate to upstream babel transformer with hermesParser forced on and + const upstream = upstreamTransformer(babelTransformerPath); + const result = await upstream.transform({ + src, + filename: transformedFilename, + options: { + ...options, + hermesParser: true, + // experimentalImportSupport: true, + }, + plugins, + }); + + // the exposed signatures for the babel transformer aren't correct, the map should be there but is not in the type definition + const withMap = result as WithMap; + if (withMap.map && transformedFilename !== filename) { + // if the filename has been transformed, change source references back to the original filename + withMap.map = patchSourceMapFilename( + withMap.map, + transformedFilename, + filename + ); + } + return result; +} diff --git a/packages/metro-serializer-esbuild/src/esbuildTransformerConfig.ts b/packages/metro-serializer-esbuild/src/esbuildTransformerConfig.ts deleted file mode 100644 index ddcd058a32..0000000000 --- a/packages/metro-serializer-esbuild/src/esbuildTransformerConfig.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { TransformerConfigT } from "metro-config"; - -export const esbuildTransformerConfig: Partial = { - getTransformOptions: async () => ({ - transform: { - /** - * Disable `import-export-plugin` to preserve ES6 import/export syntax. - * - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L315 - */ - experimentalImportSupport: false, - - /** - * Disable `inline-requires` as it is only used to inline `require()` - * calls. - * - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L319 - */ - inlineRequires: false, - }, - }), - - /** - * Minifying is unnecessary as esbuild will take care of it. - */ - minifierPath: require.resolve("./minify"), - - /** - * Metro transforms `require(...)` calls to - * `$$_REQUIRE(dependencyMap[n], ...)` in two steps. In `collectDependencies`, - * it adds the `dependencyMap[n]` parameter so the call becomes - * `require(dependencyMap[n], ...)`. Then it renames the call in - * `JsFileWrapping.wrapModule`. This flag will disable both transformations. - * - * Note that this setting is experimental and may be removed in a future - * version. - * - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L388 - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L410 - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L564 - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro/src/ModuleGraph/worker/collectDependencies.js#L467 - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro/src/ModuleGraph/worker/JsFileWrapping.js#L28 - * @see https://github.com/facebook/metro/commit/598de6f537f4d7286cee89094bcdb7101e8e4f17 - */ - unstable_disableModuleWrapping: true, - - /** - * Both of these disable the `normalizePseudoGlobals` plugin. This is needed - * to prevent Metro from renaming globals. - * - * Note that this setting is experimental and may be removed in a future - * version. - * - * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L434 - * @see https://github.com/facebook/metro/commit/5b913fa0cd30ce5b90e2b1f6318454fbdd170708 - */ - unstable_disableNormalizePseudoGlobals: true, - optimizationSizeLimit: 0, -}; diff --git a/packages/metro-serializer-esbuild/src/factory.ts b/packages/metro-serializer-esbuild/src/factory.ts new file mode 100644 index 0000000000..446cad5207 --- /dev/null +++ b/packages/metro-serializer-esbuild/src/factory.ts @@ -0,0 +1,43 @@ +import type { MetroPlugin } from "@rnx-kit/metro-serializer"; +import type { SerializerEsbuildConfig } from "@rnx-kit/types-metro-serializer-esbuild"; +import type { TransformerConfigT } from "metro-config"; +import { MetroSerializer } from "./serializer"; +import { inferBuildTarget } from "./targets"; +import { configureTransformer } from "./transformer"; + +/** + * Create factory functions for creating a Metro serializer and transformer configured to work together using esbuild. + * @param config shared configuration options for the serializer and transformer + * @returns factory functions for creating the serializer and transformer + */ +export function MetroEsbuildFactory(config: SerializerEsbuildConfig = {}) { + // set the target so we don't have to infer multiple times + config.target ??= inferBuildTarget(); + + // serializer factory function + const serializerOptions = getSerializerOptions(config); + function makeSerializer(plugins: MetroPlugin[] = []) { + return MetroSerializer(plugins, serializerOptions); + } + + // transformer configuration, right now this is simple but it will get more complex when esbuild transformation is added + function makeTransformer(userOptions?: Partial) { + return configureTransformer(config, userOptions); + } + + return { makeSerializer, makeTransformer }; +} + +function getSerializerOptions( + config: SerializerEsbuildConfig +): SerializerEsbuildConfig { + const { minifyStrategy = "serializer", ...buildOptions } = config; + // if esbuild should not minify, then disable all minification options + if (minifyStrategy !== "serializer") { + buildOptions.minify = false; + buildOptions.minifyWhitespace = false; + buildOptions.minifyIdentifiers = false; + buildOptions.minifySyntax = false; + } + return buildOptions; +} diff --git a/packages/metro-serializer-esbuild/src/index.ts b/packages/metro-serializer-esbuild/src/index.ts index 6e67611632..5c2f2af310 100644 --- a/packages/metro-serializer-esbuild/src/index.ts +++ b/packages/metro-serializer-esbuild/src/index.ts @@ -1,337 +1,4 @@ -import { info, warn } from "@rnx-kit/console"; -import type { MetroPlugin } from "@rnx-kit/metro-serializer"; -import { requireModuleFromMetro } from "@rnx-kit/tools-react-native/metro"; -import type { SerializerEsbuildOptions } from "@rnx-kit/types-metro-serializer-esbuild"; -import type { BuildResult, Plugin } from "esbuild"; -import * as esbuild from "esbuild"; -import type { SerializerConfigT } from "metro-config"; -import type { SerializerOptions } from "metro/private/DeltaBundler/types"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { - getModulePath, - getSideEffects, - isImporting, - outputOf, -} from "./module.ts"; -import { absolutizeSourceMap } from "./sourceMap.ts"; -import { inferBuildTarget } from "./targets.ts"; -import { assertVersion } from "./version.ts"; +export { configureTransformer, esbuildTransformerConfig } from "./transformer"; -export { esbuildTransformerConfig } from "./esbuildTransformerConfig.ts"; - -function escapePath(path: string): string { - return path.replace(/\\+/g, "\\\\"); -} - -function isRedundantPolyfill(modulePath: string): boolean { - // __prelude__: The content of `__prelude__` is passed to esbuild with `define` - // polyfills/require.js: `require` is already provided by esbuild - return /(?:__prelude__|[/\\]polyfills[/\\]require.js)$/.test(modulePath); -} - -/** - * esbuild bundler for Metro. - */ -export function MetroSerializer( - metroPlugins: MetroPlugin[] = [], - buildOptions?: SerializerEsbuildOptions -): SerializerConfigT["customSerializer"] { - assertVersion("0.66.1"); - - // Signal to every plugin that we're using esbuild. - process.env["RNX_METRO_SERIALIZER_ESBUILD"] = "true"; - - const baseJSBundle = requireModuleFromMetro( - "metro/src/DeltaBundler/Serializers/baseJSBundle" - ); - const bundleToString = requireModuleFromMetro("metro/src/lib/bundleToString"); - - return (entryPoint, preModules, graph, options: SerializerOptions) => { - for (const plugin of metroPlugins) { - plugin(entryPoint, preModules, graph, options); - } - - if (options.dev) { - const bundle = baseJSBundle(entryPoint, preModules, graph, options); - const bundleCode = bundleToString(bundle).code; - return Promise.resolve(bundleCode); - } - - const prelude = "__rnx_prelude__"; - - // Hermes only implements select ES6 features and is missing others like - // block scoping (https://github.com/facebook/hermes/issues/575). As of - // esbuild 0.14.49, we can use the `hermes` target instead of `es5`. Note - // that this target is somewhat conservative and may require additional - // Babel plugins. - const target = buildOptions?.target ?? inferBuildTarget(); - - const { dependencies } = graph; - const metroPlugin: Plugin = { - name: require("../package.json").name, - setup: (build) => { - // Don't add namespace to all files. esbuild currently adds it to all - // file paths in the source map. See - // https://github.com/evanw/esbuild/issues/2283. - const namespace = "virtual:metro"; - const pluginOptions = { filter: /.*/ }; - - // Metro does not inject `"use strict"`, but esbuild does. We can strip - // them out like Metro does, but it'll break the source map. See also - // https://github.com/facebook/metro/blob/0fe1253cc4f76aa2a7683cfb2ad0253d0a768c83/packages/metro-react-native-babel-preset/src/configs/main.js#L68 - if (!options.dev && buildOptions?.strictMode === false) { - const encoder = new TextEncoder(); - build.onEnd(({ outputFiles }) => { - if (outputFiles) { - const length = outputFiles.length; - for (let i = 0; i < length; ++i) { - const { hash, path, text } = outputFiles[i]; - const newText = text.replace(/"use strict";\s*/g, ""); - outputFiles[i] = { - path, - contents: encoder.encode(newText), - hash, - text: newText, - }; - } - } - }); - } - - build.onResolve(pluginOptions, (args) => { - if (dependencies.has(args.path)) { - return { - path: args.path, - sideEffects: getSideEffects(args.path), - pluginData: args.pluginData, - }; - } - - const parent = dependencies.get(args.importer); - if (parent) { - const path = getModulePath(args.path, parent); - return { - path, - sideEffects: path ? getSideEffects(path) : undefined, - pluginData: args.pluginData, - }; - } - - if (preModules.find(({ path }) => path === args.path)) { - // In certain setups, such as when using external bundles, we may - // pass virtual files here. If so, we'll need to inherit the - // namespace of the top-level prelude. - return { - path: args.path, - namespace: fs.existsSync(args.path) ? "" : args.namespace, - pluginData: args.pluginData, - }; - } - - if (args.path === prelude) { - return { - path: args.path, - namespace, - pluginData: args.pluginData, - }; - } - - throw new Error( - `Could not resolve '${args.path}' from '${args.importer}'` - ); - }); - - build.onLoad(pluginOptions, (args) => { - // Ideally, we should be adding external files to the options object - // that we pass to `esbuild.build()` below. Since it doesn't work for - // some reason, we'll filter them out here instead. - if ( - buildOptions?.fabric === false && - args.path.endsWith("ReactFabric-prod.js") - ) { - return { contents: "" }; - } - - const mod = dependencies.get(args.path); - if (mod) { - return { - contents: outputOf(mod, buildOptions?.logLevel) ?? "", - pluginData: args.pluginData, - }; - } - - const polyfill = preModules.find(({ path }) => path === args.path); - if (polyfill) { - return { - contents: outputOf(polyfill, buildOptions?.logLevel) ?? "", - pluginData: args.pluginData, - }; - } - - if (args.path === prelude) { - return { - /** - * Add all the polyfills in this file. See the `entryPoints` - * option below for more details. - * - * We must ensure that the content is ES5-friendly so esbuild - * doesn't blow up when targeting ES5, e.g. use `var` instead of - * `let` and `const`. - */ - contents: [ - /** - * Many React Native modules expect `global` to be passed with - * Metro's `require` polyfill. We need to re-create it since - * we're using esbuild's `require`. - * - * The `Function` constructor creates functions that execute in - * the global scope. We use this trait to ensure that `this` - * references the global object. - * - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function#description - */ - 'global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', - - /** Polyfills */ - ...preModules - .filter(({ path }) => !isRedundantPolyfill(path)) - .map(({ path }) => `require("${escapePath(path)}");`), - - /** - * Ensure that `react-native/Libraries/Core/InitializeCore.js` - * gets executed first. Note that this list may include modules - * from platforms other than the one we're targeting. - */ - ...options.runBeforeMainModule - .filter((value) => dependencies.has(value)) - .map((value) => `require("${escapePath(value)}");`), - - /** - * Finally, import the entry point. This must always be last. - */ - `require("${escapePath(entryPoint)}");`, - ].join("\n"), - }; - } - - warn(`No such module: ${args.path}`); - return { contents: "" }; - }); - }, - }; - - // `outfile` is required by esbuild to generate the sourcemap and insert it - // into `BuildResult["outputFiles"]`. It is also used to generate the - // `//# sourceMappingURL=` comment at the end of bundle. We've disabled - // writing to disk by setting `write: false`. Metro will handle the rest - // after we return code + sourcemap. - const outfile = - options.sourceMapUrl?.replace(/\.map$/, "") ?? "main.jsbundle"; - const sourcemapfile = options.sourceMapUrl ?? outfile + ".map"; - - const plugins = [metroPlugin]; - if (isImporting("lodash", dependencies)) { - const lodashTransformer = require("esbuild-plugin-lodash"); - plugins.push(lodashTransformer()); - } - - return esbuild - .build({ - bundle: true, - define: { - __DEV__: JSON.stringify(Boolean(options.dev)), - __METRO_GLOBAL_PREFIX__: "''", - global: "global", - }, - drop: buildOptions?.drop, - /** - * We no longer use `inject` for polyfills and `runBeforeMainModule` - * modules. A require call is generated and prepended to _all_ modules - * for each injected file. This can increase the bundle size - * significantly if there are many polyfills and modules. For just four - * polyfills (e.g. `console.js`, `error-guard.js`, `Object.es7.js`, and - * `InitializeCore.js`), we've seen an increase of ~180 KB in a small to - * medium sized app. We work around this issue by adding all the - * polyfills in a virtual file and make it the entry point. The virtual - * file then imports the actual entry point. - */ - entryPoints: [prelude], - legalComments: "none", - logLevel: buildOptions?.logLevel ?? "error", - metafile: Boolean(buildOptions?.analyze || buildOptions?.metafile), - minify: buildOptions?.minify ?? !options.dev, - minifyWhitespace: buildOptions?.minifyWhitespace, - minifyIdentifiers: buildOptions?.minifyIdentifiers, - minifySyntax: buildOptions?.minifySyntax, - outfile, - plugins, - pure: buildOptions?.pure, - sourcemap: Boolean(options.sourceMapUrl) && "linked", - target, - supported: (() => { - if (typeof target !== "string" || !target.startsWith("hermes")) { - return undefined; - } - - // `arrow` and `generator` should be safe to enable if we take into - // consideration that Hermes does not support classes. They were - // disabled in esbuild 0.14.49 after the feature compatibility table - // generator was fixed (see - // https://github.com/evanw/esbuild/releases/tag/v0.14.49). - return { - arrow: true, - "default-argument": true, - destructuring: true, - generator: true, - "rest-argument": true, - "template-literal": true, // Used heavily by `styled-components` - }; - })(), - write: false, - }) - .then(({ metafile, outputFiles }: BuildResult) => { - const result = { code: "", map: "" }; - if (outputFiles) { - for (const { path: outputPath, text } of outputFiles) { - if (outputPath === "" || outputPath.endsWith(outfile)) { - result.code = text; - } else if (outputPath.endsWith(sourcemapfile)) { - result.map = - buildOptions?.sourceMapPaths === "absolute" - ? absolutizeSourceMap(outputPath, text) - : text; - } - } - } - if (metafile) { - if (buildOptions?.analyze) { - const options = { verbose: buildOptions.analyze === "verbose" }; - esbuild - .analyzeMetafile(metafile, options) - .then((text) => info(text)); - } else { - info("esbuild bundle size:", result.code.length); - } - - if (typeof buildOptions?.metafile === "string") { - const outDir = path.dirname(sourcemapfile); - const out = path.join(outDir, buildOptions.metafile); - - info("Writing esbuild metafile to:", out); - - const metadata = - typeof metafile === "string" - ? metafile - : JSON.stringify(metafile); - fs.writeFile(out, metadata, () => { - info("Done writing esbuild metafile"); - }); - } - } else { - info("esbuild bundle size:", result.code.length); - } - return result; - }); - }; -} +export { MetroSerializer } from "./serializer"; +export { MetroEsbuildFactory } from "./factory"; diff --git a/packages/metro-serializer-esbuild/src/options.ts b/packages/metro-serializer-esbuild/src/options.ts new file mode 100644 index 0000000000..08b49b3ff2 --- /dev/null +++ b/packages/metro-serializer-esbuild/src/options.ts @@ -0,0 +1,95 @@ +import type { + SerializerEsbuildConfig, + TransformerBuildOptions, +} from "@rnx-kit/types-metro-serializer-esbuild"; +import type { BuildOptions } from "esbuild"; +import type { TransformerConfigT } from "metro-config"; + +export function getSupported( + target: string | string[] +): BuildOptions["supported"] { + const targets = Array.isArray(target) ? target : [target]; + if (!targets.some((t) => t.startsWith("hermes"))) { + return undefined; + } + return { + // test adding this one + // "const-and-let": true, + arrow: true, + "default-argument": true, + destructuring: true, + generator: true, + "rest-argument": true, + "template-literal": true, + }; +} + +export function getDefine({ dev }: { dev: boolean }): BuildOptions["define"] { + return { + __DEV__: JSON.stringify(Boolean(dev)), + __METRO_GLOBAL_PREFIX__: "''", + global: "global", + }; +} + +export type EsbuildTransformOptions = TransformerBuildOptions & { + /** + * Remember the upstream transformer if set + */ + babelTransformerPath?: TransformerConfigT["babelTransformerPath"]; +}; + +const transformerOptionKeys: (keyof TransformerBuildOptions)[] = [ + "jsx", + "jsxDev", + "jsxFactory", + "jsxFragment", + "jsxImportSource", + "logLevel", + "target", +] as const; + +export const CUSTOM_OPTIONS_KEY = "esbuildTransformer"; + +export function createEsbuildTransformOptions( + config: SerializerEsbuildConfig, + transformOptions: Partial = {} +): EsbuildTransformOptions | undefined { + if (!config.transformWithEsbuild) { + return undefined; + } + const customOptions = extractObjectValues( + config, + transformerOptionKeys + ) as EsbuildTransformOptions; + if (transformOptions.babelTransformerPath) { + customOptions.babelTransformerPath = transformOptions.babelTransformerPath; + } + return customOptions; +} + +export function getEsbuildTransformOptions( + customTransformOptions?: Record +): EsbuildTransformOptions { + const raw = customTransformOptions?.[CUSTOM_OPTIONS_KEY]; + if (typeof raw === "object" && raw !== null) { + return raw as EsbuildTransformOptions; + } + return {}; +} + +function extractObjectValues( + config: T, + keyList: (keyof T)[], + excludeInstead?: boolean +): Partial { + const options: Partial = {}; + for (const key in config) { + const inKeyList = keyList.includes(key); + const shouldInclude = excludeInstead ? !inKeyList : inKeyList; + if (shouldInclude && config[key] !== undefined) { + options[key] = config[key]; + } + } + return options; +} diff --git a/packages/metro-serializer-esbuild/src/serializer.ts b/packages/metro-serializer-esbuild/src/serializer.ts new file mode 100644 index 0000000000..60100f5343 --- /dev/null +++ b/packages/metro-serializer-esbuild/src/serializer.ts @@ -0,0 +1,333 @@ +import { info, warn } from "@rnx-kit/console"; +import type { MetroPlugin } from "@rnx-kit/metro-serializer"; +import { requireModuleFromMetro } from "@rnx-kit/tools-react-native/metro"; +import type { SerializerEsbuildOptions } from "@rnx-kit/types-metro-serializer-esbuild"; +import type { BuildResult, Plugin } from "esbuild"; +import * as esbuild from "esbuild"; +import type { SerializerConfigT } from "metro-config"; +import type { SerializerOptions } from "metro/private/DeltaBundler/types"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { + getModulePath, + getSideEffects, + isImporting, + outputOf, +} from "./module.ts"; +import { getDefine } from "./options.ts"; +import { absolutizeSourceMap } from "./sourceMap.ts"; +import { inferBuildTarget } from "./targets.ts"; +import { assertVersion } from "./version.ts"; + +function escapePath(path: string): string { + return path.replace(/\\+/g, "\\\\"); +} + +function isRedundantPolyfill(modulePath: string): boolean { + // __prelude__: The content of `__prelude__` is passed to esbuild with `define` + // polyfills/require.js: `require` is already provided by esbuild + return /(?:__prelude__|[/\\]polyfills[/\\]require.js)$/.test(modulePath); +} + +/** + * esbuild bundler for Metro. + */ +export function MetroSerializer( + metroPlugins: MetroPlugin[] = [], + buildOptions?: SerializerEsbuildOptions +): SerializerConfigT["customSerializer"] { + assertVersion("0.66.1"); + + // Signal to every plugin that we're using esbuild. + process.env["RNX_METRO_SERIALIZER_ESBUILD"] = "true"; + + const baseJSBundle = requireModuleFromMetro( + "metro/src/DeltaBundler/Serializers/baseJSBundle" + ); + const bundleToString = requireModuleFromMetro("metro/src/lib/bundleToString"); + + return (entryPoint, preModules, graph, options: SerializerOptions) => { + for (const plugin of metroPlugins) { + plugin(entryPoint, preModules, graph, options); + } + + if (options.dev) { + const bundle = baseJSBundle(entryPoint, preModules, graph, options); + const bundleCode = bundleToString(bundle).code; + return Promise.resolve(bundleCode); + } + + const prelude = "__rnx_prelude__"; + + // Hermes only implements select ES6 features and is missing others like + // block scoping (https://github.com/facebook/hermes/issues/575). As of + // esbuild 0.14.49, we can use the `hermes` target instead of `es5`. Note + // that this target is somewhat conservative and may require additional + // Babel plugins. + const target = buildOptions?.target ?? inferBuildTarget(); + + const { dependencies } = graph; + const metroPlugin: Plugin = { + name: require("../package.json").name, + setup: (build) => { + // Don't add namespace to all files. esbuild currently adds it to all + // file paths in the source map. See + // https://github.com/evanw/esbuild/issues/2283. + const namespace = "virtual:metro"; + const pluginOptions = { filter: /.*/ }; + + // Metro does not inject `"use strict"`, but esbuild does. We can strip + // them out like Metro does, but it'll break the source map. See also + // https://github.com/facebook/metro/blob/0fe1253cc4f76aa2a7683cfb2ad0253d0a768c83/packages/metro-react-native-babel-preset/src/configs/main.js#L68 + if (!options.dev && buildOptions?.strictMode === false) { + const encoder = new TextEncoder(); + build.onEnd(({ outputFiles }) => { + if (outputFiles) { + const length = outputFiles.length; + for (let i = 0; i < length; ++i) { + const { hash, path, text } = outputFiles[i]; + const newText = text.replace(/"use strict";\s*/g, ""); + outputFiles[i] = { + path, + contents: encoder.encode(newText), + hash, + text: newText, + }; + } + } + }); + } + + build.onResolve(pluginOptions, (args) => { + if (dependencies.has(args.path)) { + return { + path: args.path, + sideEffects: getSideEffects(args.path), + pluginData: args.pluginData, + }; + } + + const parent = dependencies.get(args.importer); + if (parent) { + const path = getModulePath(args.path, parent); + return { + path, + sideEffects: path ? getSideEffects(path) : undefined, + pluginData: args.pluginData, + }; + } + + if (preModules.find(({ path }) => path === args.path)) { + // In certain setups, such as when using external bundles, we may + // pass virtual files here. If so, we'll need to inherit the + // namespace of the top-level prelude. + return { + path: args.path, + namespace: fs.existsSync(args.path) ? "" : args.namespace, + pluginData: args.pluginData, + }; + } + + if (args.path === prelude) { + return { + path: args.path, + namespace, + pluginData: args.pluginData, + }; + } + + throw new Error( + `Could not resolve '${args.path}' from '${args.importer}'` + ); + }); + + build.onLoad(pluginOptions, (args) => { + // Ideally, we should be adding external files to the options object + // that we pass to `esbuild.build()` below. Since it doesn't work for + // some reason, we'll filter them out here instead. + if ( + buildOptions?.fabric === false && + args.path.endsWith("ReactFabric-prod.js") + ) { + return { contents: "" }; + } + + const mod = dependencies.get(args.path); + if (mod) { + return { + contents: outputOf(mod, buildOptions?.logLevel) ?? "", + pluginData: args.pluginData, + }; + } + + const polyfill = preModules.find(({ path }) => path === args.path); + if (polyfill) { + return { + contents: outputOf(polyfill, buildOptions?.logLevel) ?? "", + pluginData: args.pluginData, + }; + } + + if (args.path === prelude) { + return { + /** + * Add all the polyfills in this file. See the `entryPoints` + * option below for more details. + * + * We must ensure that the content is ES5-friendly so esbuild + * doesn't blow up when targeting ES5, e.g. use `var` instead of + * `let` and `const`. + */ + contents: [ + /** + * Many React Native modules expect `global` to be passed with + * Metro's `require` polyfill. We need to re-create it since + * we're using esbuild's `require`. + * + * The `Function` constructor creates functions that execute in + * the global scope. We use this trait to ensure that `this` + * references the global object. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function#description + */ + 'global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', + + /** Polyfills */ + ...preModules + .filter(({ path }) => !isRedundantPolyfill(path)) + .map(({ path }) => `require("${escapePath(path)}");`), + + /** + * Ensure that `react-native/Libraries/Core/InitializeCore.js` + * gets executed first. Note that this list may include modules + * from platforms other than the one we're targeting. + */ + ...options.runBeforeMainModule + .filter((value) => dependencies.has(value)) + .map((value) => `require("${escapePath(value)}");`), + + /** + * Finally, import the entry point. This must always be last. + */ + `require("${escapePath(entryPoint)}");`, + ].join("\n"), + }; + } + + warn(`No such module: ${args.path}`); + return { contents: "" }; + }); + }, + }; + + // `outfile` is required by esbuild to generate the sourcemap and insert it + // into `BuildResult["outputFiles"]`. It is also used to generate the + // `//# sourceMappingURL=` comment at the end of bundle. We've disabled + // writing to disk by setting `write: false`. Metro will handle the rest + // after we return code + sourcemap. + const outfile = + options.sourceMapUrl?.replace(/\.map$/, "") ?? "main.jsbundle"; + const sourcemapfile = options.sourceMapUrl ?? outfile + ".map"; + + const plugins = [metroPlugin]; + if (isImporting("lodash", dependencies)) { + const lodashTransformer = require("esbuild-plugin-lodash"); + plugins.push(lodashTransformer()); + } + + console.log("Building to target:", target); + return esbuild + .build({ + bundle: true, + define: getDefine(options), + drop: buildOptions?.drop, + /** + * We no longer use `inject` for polyfills and `runBeforeMainModule` + * modules. A require call is generated and prepended to _all_ modules + * for each injected file. This can increase the bundle size + * significantly if there are many polyfills and modules. For just four + * polyfills (e.g. `console.js`, `error-guard.js`, `Object.es7.js`, and + * `InitializeCore.js`), we've seen an increase of ~180 KB in a small to + * medium sized app. We work around this issue by adding all the + * polyfills in a virtual file and make it the entry point. The virtual + * file then imports the actual entry point. + */ + entryPoints: [prelude], + legalComments: "none", + logLevel: buildOptions?.logLevel ?? "error", + metafile: Boolean(buildOptions?.analyze || buildOptions?.metafile), + minify: buildOptions?.minify ?? !options.dev, + minifyWhitespace: buildOptions?.minifyWhitespace, + minifyIdentifiers: buildOptions?.minifyIdentifiers, + minifySyntax: buildOptions?.minifySyntax, + outfile, + plugins, + pure: buildOptions?.pure, + sourcemap: Boolean(options.sourceMapUrl) && "linked", + target, + supported: (() => { + if (typeof target !== "string" || !target.startsWith("hermes")) { + return undefined; + } + + // `arrow` and `generator` should be safe to enable if we take into + // consideration that Hermes does not support classes. They were + // disabled in esbuild 0.14.49 after the feature compatibility table + // generator was fixed (see + // https://github.com/evanw/esbuild/releases/tag/v0.14.49). + return { + arrow: true, + "default-argument": true, + destructuring: true, + generator: true, + "rest-argument": true, + "template-literal": true, // Used heavily by `styled-components` + }; + })(), + write: false, + }) + .then(({ metafile, outputFiles }: BuildResult) => { + const result = { code: "", map: "" }; + if (outputFiles) { + for (const { path: outputPath, text } of outputFiles) { + if (outputPath === "" || outputPath.endsWith(outfile)) { + result.code = text; + } else if (outputPath.endsWith(sourcemapfile)) { + result.map = + buildOptions?.sourceMapPaths === "absolute" + ? absolutizeSourceMap(outputPath, text) + : text; + } + } + } + if (metafile) { + if (buildOptions?.analyze) { + const options = { verbose: buildOptions.analyze === "verbose" }; + esbuild + .analyzeMetafile(metafile, options) + .then((text) => info(text)); + } else { + info("esbuild bundle size:", result.code.length); + } + + if (typeof buildOptions?.metafile === "string") { + const outDir = path.dirname(sourcemapfile); + const out = path.join(outDir, buildOptions.metafile); + + info("Writing esbuild metafile to:", out); + + const metadata = + typeof metafile === "string" + ? metafile + : JSON.stringify(metafile); + fs.writeFile(out, metadata, () => { + info("Done writing esbuild metafile"); + }); + } + } else { + info("esbuild bundle size:", result.code.length); + } + return result; + }); + }; +} diff --git a/packages/metro-serializer-esbuild/src/sourceMap.ts b/packages/metro-serializer-esbuild/src/sourceMap.ts index 4de26ba2f7..71b29adb51 100644 --- a/packages/metro-serializer-esbuild/src/sourceMap.ts +++ b/packages/metro-serializer-esbuild/src/sourceMap.ts @@ -41,3 +41,19 @@ export function getInlineSourceMappingURL(modules: readonly Module[]): string { export function generateSourceMappingURL(modules: readonly Module[]): string { return `//# sourceMappingURL=${getInlineSourceMappingURL(modules)}`; } + +export function patchSourceMapFilename< + T extends { sources?: string[] } | string, +>(sourceMap: T, virtualName: string, correctName: string): T { + const asObj = + typeof sourceMap === "string" ? JSON.parse(sourceMap) : sourceMap; + const { sources } = asObj ?? {}; + if (sources) { + for (let i = 0; i < sources.length; i++) { + if (sources[i] === virtualName) { + sources[i] = correctName; + } + } + } + return typeof sourceMap === "string" ? (JSON.stringify(asObj) as T) : asObj; +} diff --git a/packages/metro-serializer-esbuild/src/transformer.ts b/packages/metro-serializer-esbuild/src/transformer.ts new file mode 100644 index 0000000000..8128419086 --- /dev/null +++ b/packages/metro-serializer-esbuild/src/transformer.ts @@ -0,0 +1,134 @@ +import type { SerializerEsbuildConfig } from "@rnx-kit/types-metro-serializer-esbuild"; +import type { + TransformerConfigT, + GetTransformOptions, + ExtraTransformOptions, +} from "metro-config"; +import { + createEsbuildTransformOptions, + type EsbuildTransformOptions, + CUSTOM_OPTIONS_KEY, +} from "./options"; + +type ExtendedTransformOptions = ExtraTransformOptions & { + transform?: ExtraTransformOptions["transform"] & { + customTransformOptions?: Record; + }; +}; + +export function createGetTransformOptions( + upstream?: GetTransformOptions, + customOptions?: EsbuildTransformOptions +): GetTransformOptions { + return async (entryPoints, options, getDependenciesOf) => { + const upstreamOptions: ExtendedTransformOptions = upstream + ? await upstream(entryPoints, options, getDependenciesOf) + : {}; + + return { + ...upstreamOptions, + transform: { + ...upstreamOptions.transform, + + /** + * Disable `import-export-plugin` to preserve ES6 import/export syntax. + * + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L315 + */ + experimentalImportSupport: false, + + /** + * Disable `inline-requires` as it is only used to inline `require()` + * calls. + * + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L319 + */ + inlineRequires: false, + + /** + * Add custom transformer options if any need to be stored and serialized to the transformers + */ + ...(customOptions && { + customTransformOptions: { + ...upstreamOptions.transform?.customTransformOptions, + [CUSTOM_OPTIONS_KEY]: customOptions, + }, + }), + }, + }; + }; +} + +export const esbuildTransformerConfig: Partial = { + getTransformOptions: createGetTransformOptions(), + + /** + * Minifying is unnecessary as esbuild will take care of it. + */ + minifierPath: require.resolve("./minify"), + + /** + * Metro transforms `require(...)` calls to + * `$$_REQUIRE(dependencyMap[n], ...)` in two steps. In `collectDependencies`, + * it adds the `dependencyMap[n]` parameter so the call becomes + * `require(dependencyMap[n], ...)`. Then it renames the call in + * `JsFileWrapping.wrapModule`. This flag will disable both transformations. + * + * Note that this setting is experimental and may be removed in a future + * version. + * + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L388 + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L410 + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L564 + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro/src/ModuleGraph/worker/collectDependencies.js#L467 + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro/src/ModuleGraph/worker/JsFileWrapping.js#L28 + * @see https://github.com/facebook/metro/commit/598de6f537f4d7286cee89094bcdb7101e8e4f17 + */ + unstable_disableModuleWrapping: true, + + /** + * Both of these disable the `normalizePseudoGlobals` plugin. This is needed + * to prevent Metro from renaming globals. + * + * Note that this setting is experimental and may be removed in a future + * version. + * + * @see https://github.com/facebook/metro/blob/598de6f537f4d7286cee89094bcdb7101e8e4f17/packages/metro-transform-worker/src/index.js#L434 + * @see https://github.com/facebook/metro/commit/5b913fa0cd30ce5b90e2b1f6318454fbdd170708 + */ + unstable_disableNormalizePseudoGlobals: true, + optimizationSizeLimit: 0, +}; + +export function configureTransformer( + config: SerializerEsbuildConfig = {}, + userOptions?: Partial +): Partial { + // this will only be defined if we are using esbuild as a front end transformer + const customOptions = createEsbuildTransformOptions(config, userOptions); + + // configure the getTransformOptions to set the required values while passing through the user configured values. + // if customOptions are needed they will also be returned via this function to be passed to transformer workers + const getTransformOptions = createGetTransformOptions( + userOptions?.getTransformOptions, + customOptions + ); + + // now return the built up transformer configuration + return { + // start with the user options as the options we need to set need to take precedence + ...userOptions, + + // set the standard esbuild transformer options + ...esbuildTransformerConfig, + + // finish with the custom getTransformOptions that handles both upstream settings and custom transformer options + getTransformOptions, + + // if esbuild transformation is enabled, use our custom transformer instead of upstream. The previous babelTransformerPath + // (if set) will have been stored in customOptions to be used by the esbuild transformer + ...(customOptions && { + babelTransformerPath: require.resolve("./esbuildTransformer"), + }), + }; +} diff --git a/packages/metro-serializer-esbuild/test/const.ts b/packages/metro-serializer-esbuild/test/const.ts new file mode 100644 index 0000000000..be7606b41d --- /dev/null +++ b/packages/metro-serializer-esbuild/test/const.ts @@ -0,0 +1,175 @@ +export const DIRECT_PATH = "test/__fixtures__/direct.ts"; +export const DIRECT_RESULT = [ + "(() => {", + " var __defProp = Object.defineProperty;", + " var __getOwnPropDesc = Object.getOwnPropertyDescriptor;", + " var __getOwnPropNames = Object.getOwnPropertyNames;", + " var __hasOwnProp = Object.prototype.hasOwnProperty;", + " var __esm = (fn, res) => function __init() {", + " return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;", + " };", + " var __copyProps = (to, from, except, desc) => {", + ' if (from && typeof from === "object" || typeof from === "function")', + " for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {", + " key = keys[i];", + " if (!__hasOwnProp.call(to, key) && key !== except)", + " __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });", + " }", + " return to;", + " };", + ' var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);', + "", + " // test/__fixtures__/base.ts", + " function app() {", + ' "this should _not_ be removed";', + " }", + " var init_base = __esm({", + ' "test/__fixtures__/base.ts"() {', + ' "use strict";', + " }", + " });", + "", + " // test/__fixtures__/direct.ts", + " var direct_exports = {};", + " var init_direct = __esm({", + ' "test/__fixtures__/direct.ts"() {', + ' "use strict";', + " init_base();", + " app();", + " }", + " });", + "", + " // virtual:metro:__rnx_prelude__", + ' global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', + " init_direct();", + "})();", + "", +]; + +export const EXPORT_ALL_PATH = "test/__fixtures__/exportAll.ts"; +export const EXPORT_ALL_RESULT = [ + "(() => {", + " var __defProp = Object.defineProperty;", + " var __getOwnPropDesc = Object.getOwnPropertyDescriptor;", + " var __getOwnPropNames = Object.getOwnPropertyNames;", + " var __hasOwnProp = Object.prototype.hasOwnProperty;", + " var __esm = (fn, res) => function __init() {", + " return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;", + " };", + " var __copyProps = (to, from, except, desc) => {", + ' if (from && typeof from === "object" || typeof from === "function")', + " for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {", + " key = keys[i];", + " if (!__hasOwnProp.call(to, key) && key !== except)", + " __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });", + " }", + " return to;", + " };", + ' var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);', + "", + " // test/__fixtures__/base.ts", + " function app() {", + ' "this should _not_ be removed";', + " }", + " var init_base = __esm({", + ' "test/__fixtures__/base.ts"() {', + ' "use strict";', + " }", + " });", + "", + " // test/__fixtures__/exportAllPublic.ts", + " var init_exportAllPublic = __esm({", + ' "test/__fixtures__/exportAllPublic.ts"() {', + ' "use strict";', + " init_base();", + " }", + " });", + "", + " // test/__fixtures__/exportAll.ts", + " var exportAll_exports = {};", + " var init_exportAll = __esm({", + ' "test/__fixtures__/exportAll.ts"() {', + ' "use strict";', + " init_exportAllPublic();", + " app();", + " }", + " });", + "", + " // virtual:metro:__rnx_prelude__", + ' global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', + " init_exportAll();", + "})();", + "", +]; + +export const NESTED_EXPORT_ALL_PATH = "test/__fixtures__/nestedExportAll.ts"; +export const NESTED_EXPORT_ALL_RESULT = [ + "(() => {", + " var __defProp = Object.defineProperty;", + " var __getOwnPropDesc = Object.getOwnPropertyDescriptor;", + " var __getOwnPropNames = Object.getOwnPropertyNames;", + " var __hasOwnProp = Object.prototype.hasOwnProperty;", + " var __esm = (fn, res) => function __init() {", + " return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;", + " };", + " var __copyProps = (to, from, except, desc) => {", + ' if (from && typeof from === "object" || typeof from === "function")', + " for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {", + " key = keys[i];", + " if (!__hasOwnProp.call(to, key) && key !== except)", + " __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });", + " }", + " return to;", + " };", + ' var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);', + "", + " // test/__fixtures__/base.ts", + " function app() {", + ' "this should _not_ be removed";', + " }", + " var init_base = __esm({", + ' "test/__fixtures__/base.ts"() {', + ' "use strict";', + " }", + " });", + "", + " // test/__fixtures__/exportAllPublic.ts", + " var init_exportAllPublic = __esm({", + ' "test/__fixtures__/exportAllPublic.ts"() {', + ' "use strict";', + " init_base();", + " }", + " });", + "", + " // test/__fixtures__/nestedExportAllInternal.ts", + " var init_nestedExportAllInternal = __esm({", + ' "test/__fixtures__/nestedExportAllInternal.ts"() {', + ' "use strict";', + " init_exportAllPublic();", + " }", + " });", + "", + " // test/__fixtures__/nestedExportAllPublic.ts", + " var init_nestedExportAllPublic = __esm({", + ' "test/__fixtures__/nestedExportAllPublic.ts"() {', + ' "use strict";', + " init_nestedExportAllInternal();", + " }", + " });", + "", + " // test/__fixtures__/nestedExportAll.ts", + " var nestedExportAll_exports = {};", + " var init_nestedExportAll = __esm({", + ' "test/__fixtures__/nestedExportAll.ts"() {', + ' "use strict";', + " init_nestedExportAllPublic();", + " app();", + " }", + " });", + "", + " // virtual:metro:__rnx_prelude__", + ' global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', + " init_nestedExportAll();", + "})();", + "", +]; diff --git a/packages/metro-serializer-esbuild/test/index.test.ts b/packages/metro-serializer-esbuild/test/index.test.ts index 35d2de6fc0..4a3d6f1d97 100644 --- a/packages/metro-serializer-esbuild/test/index.test.ts +++ b/packages/metro-serializer-esbuild/test/index.test.ts @@ -7,6 +7,19 @@ import { createRequire } from "node:module"; import * as path from "node:path"; import { after, before, describe, it } from "node:test"; import { URL, fileURLToPath } from "node:url"; +import { + DIRECT_PATH, + DIRECT_RESULT, + EXPORT_ALL_PATH, + EXPORT_ALL_RESULT, + NESTED_EXPORT_ALL_PATH, + NESTED_EXPORT_ALL_RESULT, +} from "./const.ts"; + +const transformConfig = path.join( + fileURLToPath(new URL("..", import.meta.url)), + "metro.transform.config.js" +); async function buildBundle( args: BundleArgs, @@ -37,7 +50,8 @@ describe("metro-serializer-esbuild", () => { async function bundle( entryFile: string, dev = false, - sourcemapOutput: string | undefined = undefined + sourcemapOutput: string | undefined = undefined, + transform = false ): Promise { let result = ""; await buildBundle( @@ -46,6 +60,7 @@ describe("metro-serializer-esbuild", () => { bundleEncoding: "utf8", bundleOutput: ".test-output.jsbundle", dev, + config: transform ? transformConfig : undefined, platform: "ios", resetCache: true, resetGlobalCache: false, @@ -78,185 +93,23 @@ describe("metro-serializer-esbuild", () => { } it("removes unused code", async () => { - const result = await bundle("test/__fixtures__/direct.ts"); - deepEqual(result, [ - "(() => {", - " var __defProp = Object.defineProperty;", - " var __getOwnPropDesc = Object.getOwnPropertyDescriptor;", - " var __getOwnPropNames = Object.getOwnPropertyNames;", - " var __hasOwnProp = Object.prototype.hasOwnProperty;", - " var __esm = (fn, res) => function __init() {", - " return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;", - " };", - " var __copyProps = (to, from, except, desc) => {", - ' if (from && typeof from === "object" || typeof from === "function")', - " for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {", - " key = keys[i];", - " if (!__hasOwnProp.call(to, key) && key !== except)", - " __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });", - " }", - " return to;", - " };", - ' var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);', - "", - " // test/__fixtures__/base.ts", - " function app() {", - ' "this should _not_ be removed";', - " }", - " var init_base = __esm({", - ' "test/__fixtures__/base.ts"() {', - ' "use strict";', - " }", - " });", - "", - " // test/__fixtures__/direct.ts", - " var direct_exports = {};", - " var init_direct = __esm({", - ' "test/__fixtures__/direct.ts"() {', - ' "use strict";', - " init_base();", - " app();", - " }", - " });", - "", - " // virtual:metro:__rnx_prelude__", - ' global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', - " init_direct();", - "})();", - "", - ]); + const result = await bundle(DIRECT_PATH); + deepEqual(result, DIRECT_RESULT); + }); + + it("removes unused code with transform", async () => { + const result = await bundle(DIRECT_PATH, false, undefined, true); + deepEqual(result, DIRECT_RESULT); }); it("removes unused code (export *)", async () => { - const result = await bundle("test/__fixtures__/exportAll.ts"); - deepEqual(result, [ - "(() => {", - " var __defProp = Object.defineProperty;", - " var __getOwnPropDesc = Object.getOwnPropertyDescriptor;", - " var __getOwnPropNames = Object.getOwnPropertyNames;", - " var __hasOwnProp = Object.prototype.hasOwnProperty;", - " var __esm = (fn, res) => function __init() {", - " return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;", - " };", - " var __copyProps = (to, from, except, desc) => {", - ' if (from && typeof from === "object" || typeof from === "function")', - " for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {", - " key = keys[i];", - " if (!__hasOwnProp.call(to, key) && key !== except)", - " __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });", - " }", - " return to;", - " };", - ' var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);', - "", - " // test/__fixtures__/base.ts", - " function app() {", - ' "this should _not_ be removed";', - " }", - " var init_base = __esm({", - ' "test/__fixtures__/base.ts"() {', - ' "use strict";', - " }", - " });", - "", - " // test/__fixtures__/exportAllPublic.ts", - " var init_exportAllPublic = __esm({", - ' "test/__fixtures__/exportAllPublic.ts"() {', - ' "use strict";', - " init_base();", - " }", - " });", - "", - " // test/__fixtures__/exportAll.ts", - " var exportAll_exports = {};", - " var init_exportAll = __esm({", - ' "test/__fixtures__/exportAll.ts"() {', - ' "use strict";', - " init_exportAllPublic();", - " app();", - " }", - " });", - "", - " // virtual:metro:__rnx_prelude__", - ' global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', - " init_exportAll();", - "})();", - "", - ]); + const result = await bundle(EXPORT_ALL_PATH); + deepEqual(result, EXPORT_ALL_RESULT); }); it("removes unused code (nested export *)", async () => { - const result = await bundle("test/__fixtures__/nestedExportAll.ts"); - deepEqual(result, [ - "(() => {", - " var __defProp = Object.defineProperty;", - " var __getOwnPropDesc = Object.getOwnPropertyDescriptor;", - " var __getOwnPropNames = Object.getOwnPropertyNames;", - " var __hasOwnProp = Object.prototype.hasOwnProperty;", - " var __esm = (fn, res) => function __init() {", - " return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;", - " };", - " var __copyProps = (to, from, except, desc) => {", - ' if (from && typeof from === "object" || typeof from === "function")', - " for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {", - " key = keys[i];", - " if (!__hasOwnProp.call(to, key) && key !== except)", - " __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });", - " }", - " return to;", - " };", - ' var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);', - "", - " // test/__fixtures__/base.ts", - " function app() {", - ' "this should _not_ be removed";', - " }", - " var init_base = __esm({", - ' "test/__fixtures__/base.ts"() {', - ' "use strict";', - " }", - " });", - "", - " // test/__fixtures__/exportAllPublic.ts", - " var init_exportAllPublic = __esm({", - ' "test/__fixtures__/exportAllPublic.ts"() {', - ' "use strict";', - " init_base();", - " }", - " });", - "", - " // test/__fixtures__/nestedExportAllInternal.ts", - " var init_nestedExportAllInternal = __esm({", - ' "test/__fixtures__/nestedExportAllInternal.ts"() {', - ' "use strict";', - " init_exportAllPublic();", - " }", - " });", - "", - " // test/__fixtures__/nestedExportAllPublic.ts", - " var init_nestedExportAllPublic = __esm({", - ' "test/__fixtures__/nestedExportAllPublic.ts"() {', - ' "use strict";', - " init_nestedExportAllInternal();", - " }", - " });", - "", - " // test/__fixtures__/nestedExportAll.ts", - " var nestedExportAll_exports = {};", - " var init_nestedExportAll = __esm({", - ' "test/__fixtures__/nestedExportAll.ts"() {', - ' "use strict";', - " init_nestedExportAllPublic();", - " app();", - " }", - " });", - "", - " // virtual:metro:__rnx_prelude__", - ' global = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : new Function("return this;")();', - " init_nestedExportAll();", - "})();", - "", - ]); + const result = await bundle(NESTED_EXPORT_ALL_PATH); + deepEqual(result, NESTED_EXPORT_ALL_RESULT); }); it("removes unused code (import *)", async () => { diff --git a/packages/types-metro-serializer-esbuild/src/compat.test.ts b/packages/types-metro-serializer-esbuild/src/compat.test.ts index 72c5261c61..25b6b9cdb3 100644 --- a/packages/types-metro-serializer-esbuild/src/compat.test.ts +++ b/packages/types-metro-serializer-esbuild/src/compat.test.ts @@ -1,5 +1,5 @@ import type { BuildOptions as EsbuildBuildOptions } from "esbuild"; -import type { BaseBuildOptions } from "./index.ts"; +import type { AllBuildOptions } from "./index.ts"; /** * These are type-level tests to ensure our BaseBuildOptions type stays compatible with esbuild's BuildOptions. @@ -20,18 +20,18 @@ type Extends = [A] extends [B] ? true : false; * - If this fails, BaseBuildOptions can no longer be passed where esbuild expects BuildOptions. */ export type BaseAssignableToEsbuild = Assert< - Extends + Extends >; /** * Check that the overlapping keys are still compatible field-by-field. * - This gives targeted failures if esbuild changes a property type. */ -type OverlapKeys = keyof BaseBuildOptions & keyof EsbuildBuildOptions; +type OverlapKeys = keyof AllBuildOptions & keyof EsbuildBuildOptions; export type OverlapStillCompatible = Assert< Extends< - { [K in OverlapKeys]: BaseBuildOptions[K] }, + { [K in OverlapKeys]: AllBuildOptions[K] }, { [K in OverlapKeys]: EsbuildBuildOptions[K] } > >; @@ -41,5 +41,5 @@ export type OverlapStillCompatible = Assert< * - This will fail if a key that esbuild *doesn't* have is included. */ export type NoExtraKeys = Assert< - Extends + Extends >; diff --git a/packages/types-metro-serializer-esbuild/src/index.ts b/packages/types-metro-serializer-esbuild/src/index.ts index 902c28551d..7bae070d26 100644 --- a/packages/types-metro-serializer-esbuild/src/index.ts +++ b/packages/types-metro-serializer-esbuild/src/index.ts @@ -25,6 +25,25 @@ export type BaseBuildOptions = { target?: string | string[]; }; +export type TransformerBuildOptions = Omit< + BaseBuildOptions, + "minify" | "minifyWhitespace" | "minifyIdentifiers" | "minifySyntax" | "pure" +> & { + /** + * JSX transform mode. Defaults to `"automatic"` (esbuild handles JSX). + * - `"automatic"` uses the React 17+ JSX transform via esbuild + * - `"transform"` uses classic createElement calls via esbuild + * - `"preserve"` leaves JSX as-is, letting the upstream babel transformer handle it, may be necessary for some babel plugins + */ + jsx?: "transform" | "preserve" | "automatic"; + jsxFactory?: string; + jsxFragment?: string; + jsxImportSource?: string; + jsxDev?: boolean; +}; + +export type AllBuildOptions = BaseBuildOptions & TransformerBuildOptions; + /** * Options for @rnx-kit/metro-serializer-esbuild plugin. */ @@ -35,3 +54,32 @@ export type SerializerEsbuildOptions = BaseBuildOptions & { sourceMapPaths?: "absolute" | "relative"; strictMode?: boolean; }; + +/** + * Options used to configure both the serializer and transformer options, so that the two can work in tandem. + */ +export type SerializerEsbuildConfig = SerializerEsbuildOptions & + TransformerBuildOptions & { + /** + * Minification strategy to use for the transformer when minification is enabled. + * - `serializer`: Minify in the serializer using esbuild. This is the default and recommended option, as it provides better + * minification results and faster build times. + * - `metro-default`: Minify in the transformer using Metro's default minifier (Terser). This is not recommended, as it can lead to worse + * minification results and slower build times, but is provided as an option for compatibility and comparison purposes. + */ + minifyStrategy?: "serializer" | "metro-default"; + + /** + * Set to false to disable tree-shaking in production builds. Use if you still want to use esbuild for minification but want to + * preserve all code paths. + * @default true in production, false in development + */ + treeShaking?: boolean; + + /** + * Enable experimental support to use esbuild as a transformer in the bundle process. This will use esbuild for transforming typescript + * and jsx files (unless jsx is set to "preserve") then will route the result through the upstream babel transformer. + * @default false + */ + transformWithEsbuild?: boolean; + }; diff --git a/yarn.lock b/yarn.lock index 1769700c6f..ee8efbc72f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4474,6 +4474,16 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-plugin-codegen@npm:0.83.3": + version: 0.83.3 + resolution: "@react-native/babel-plugin-codegen@npm:0.83.3" + dependencies: + "@babel/traverse": "npm:^7.25.3" + "@react-native/codegen": "npm:0.83.3" + checksum: 10c0/ba218eb101725da1a8940bb2b8aa5777e2b08012069754255370ad8725e9dccaf19ad7415d3cfa72e0369affbcee407b25469cca4ed693003bb1bd51aa25a9e7 + languageName: node + linkType: hard + "@react-native/babel-preset@npm:0.78.3, @react-native/babel-preset@npm:^0.78.0": version: 0.78.3 resolution: "@react-native/babel-preset@npm:0.78.3" @@ -4639,6 +4649,61 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-preset@npm:0.83.3": + version: 0.83.3 + resolution: "@react-native/babel-preset@npm:0.83.3" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-transform-arrow-functions": "npm:^7.24.7" + "@babel/plugin-transform-async-generator-functions": "npm:^7.25.4" + "@babel/plugin-transform-async-to-generator": "npm:^7.24.7" + "@babel/plugin-transform-block-scoping": "npm:^7.25.0" + "@babel/plugin-transform-class-properties": "npm:^7.25.4" + "@babel/plugin-transform-classes": "npm:^7.25.4" + "@babel/plugin-transform-computed-properties": "npm:^7.24.7" + "@babel/plugin-transform-destructuring": "npm:^7.24.8" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" + "@babel/plugin-transform-for-of": "npm:^7.24.7" + "@babel/plugin-transform-function-name": "npm:^7.25.1" + "@babel/plugin-transform-literals": "npm:^7.25.2" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.24.7" + "@babel/plugin-transform-numeric-separator": "npm:^7.24.7" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.7" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.24.7" + "@babel/plugin-transform-optional-chaining": "npm:^7.24.8" + "@babel/plugin-transform-parameters": "npm:^7.24.7" + "@babel/plugin-transform-private-methods": "npm:^7.24.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" + "@babel/plugin-transform-react-display-name": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx": "npm:^7.25.2" + "@babel/plugin-transform-react-jsx-self": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx-source": "npm:^7.24.7" + "@babel/plugin-transform-regenerator": "npm:^7.24.7" + "@babel/plugin-transform-runtime": "npm:^7.24.7" + "@babel/plugin-transform-shorthand-properties": "npm:^7.24.7" + "@babel/plugin-transform-spread": "npm:^7.24.7" + "@babel/plugin-transform-sticky-regex": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.25.2" + "@babel/plugin-transform-unicode-regex": "npm:^7.24.7" + "@babel/template": "npm:^7.25.0" + "@react-native/babel-plugin-codegen": "npm:0.83.3" + babel-plugin-syntax-hermes-parser: "npm:0.32.0" + babel-plugin-transform-flow-enums: "npm:^0.0.2" + react-refresh: "npm:^0.14.0" + peerDependencies: + "@babel/core": "*" + checksum: 10c0/0b04abbbca216add8f32e03905baaa31c10faabe6d4c244206832ad48871ccc77c3bf7524322d9916f791c9f22fcda577a90c0cc80628e4f2a7f495a02d7fe30 + languageName: node + linkType: hard + "@react-native/codegen@npm:0.78.3, @react-native/codegen@npm:^0.78.0": version: 0.78.3 resolution: "@react-native/codegen@npm:0.78.3" @@ -4688,6 +4753,23 @@ __metadata: languageName: node linkType: hard +"@react-native/codegen@npm:0.83.3": + version: 0.83.3 + resolution: "@react-native/codegen@npm:0.83.3" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/parser": "npm:^7.25.3" + glob: "npm:^7.1.1" + hermes-parser: "npm:0.32.0" + invariant: "npm:^2.2.4" + nullthrows: "npm:^1.1.1" + yargs: "npm:^17.6.2" + peerDependencies: + "@babel/core": "*" + checksum: 10c0/94e5901aac3cb7d81ef2b3c072a527925657d001dd41ffc963d17606cf5fe4933c3edf1cfca48180110e72a12eb178ef780578dd9fe4c6ed6f6a65df4986fd06 + languageName: node + linkType: hard + "@react-native/community-cli-plugin@npm:0.78.3, @react-native/community-cli-plugin@npm:^0.78.0": version: 0.78.3 resolution: "@react-native/community-cli-plugin@npm:0.78.3" @@ -4936,6 +5018,20 @@ __metadata: languageName: node linkType: hard +"@react-native/metro-babel-transformer@npm:^0.83.0": + version: 0.83.3 + resolution: "@react-native/metro-babel-transformer@npm:0.83.3" + dependencies: + "@babel/core": "npm:^7.25.2" + "@react-native/babel-preset": "npm:0.83.3" + hermes-parser: "npm:0.32.0" + nullthrows: "npm:^1.1.1" + peerDependencies: + "@babel/core": "*" + checksum: 10c0/ce919b401605220288ed209fa79b3a627f26e1002fcfe3b21a310591a303c402aecc16d8bce9aee495261730baf73c2a1411ce2c303c0ec8c8299c730ff30b34 + languageName: node + linkType: hard + "@react-native/metro-config@npm:^0.78.0": version: 0.78.3 resolution: "@react-native/metro-config@npm:0.78.3" @@ -5496,6 +5592,7 @@ __metadata: "@fluentui/utilities": "npm:8.13.9" "@react-native-community/cli-types": "npm:^20.0.0" "@react-native/babel-preset": "npm:^0.83.0" + "@react-native/metro-babel-transformer": "npm:^0.83.0" "@react-native/metro-config": "npm:^0.83.0" "@rnx-kit/babel-plugin-import-path-remapper": "npm:*" "@rnx-kit/babel-preset-metro-react-native": "npm:*" @@ -5513,6 +5610,7 @@ __metadata: esbuild-plugin-lodash: "npm:^1.2.0" lodash-es: "npm:^4.17.21" metro: "npm:^0.83.3" + metro-babel-transformer: "npm:^0.83.0" metro-config: "npm:^0.83.3" metro-transform-worker: "npm:^0.83.1" react: "npm:19.2.0" @@ -10941,6 +11039,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.33.3": + version: 0.33.3 + resolution: "hermes-estree@npm:0.33.3" + checksum: 10c0/4e04e767a706a93c59d64ef3f114075aeb93b08433655d4f11d310f0785c2a74d5b5041b80bc34d22630dece54865dd93a53fde160d48b8369cfef10dbd0520b + languageName: node + linkType: hard + "hermes-parser@npm:0.25.1": version: 0.25.1 resolution: "hermes-parser@npm:0.25.1" @@ -10968,6 +11073,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:0.33.3": + version: 0.33.3 + resolution: "hermes-parser@npm:0.33.3" + dependencies: + hermes-estree: "npm:0.33.3" + checksum: 10c0/f7d69de54c77321d8481e37a323bbac01d180ec982275ef8925ceaaf7e501fc3062593e84cf5da50852f36daffb34d0f5d6cbbef079fd0125a7b91c1fe84f225 + languageName: node + linkType: hard + "hpagent@npm:^1.2.0": version: 1.2.0 resolution: "hpagent@npm:1.2.0" @@ -12911,6 +13025,18 @@ __metadata: languageName: node linkType: hard +"metro-babel-transformer@npm:^0.83.0": + version: 0.83.5 + resolution: "metro-babel-transformer@npm:0.83.5" + dependencies: + "@babel/core": "npm:^7.25.2" + flow-enums-runtime: "npm:^0.0.6" + hermes-parser: "npm:0.33.3" + nullthrows: "npm:^1.1.1" + checksum: 10c0/b1448241d5d7a77eeca758226bde5fc44da9f2e63f4e67037c289fe006c0f047b84fc3e77be61ba14ea605b0890232813ab75b1915faad21796b9bb873458506 + languageName: node + linkType: hard + "metro-cache-key@npm:0.81.5": version: 0.81.5 resolution: "metro-cache-key@npm:0.81.5"