зеркало из https://github.com/microsoft/rnx-kit.git
fix(metro-config): workaround for Expo unintentionally overwriting fields (#3266)
This commit is contained in:
Родитель
71156f4a8d
Коммит
f0a85ac855
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"@rnx-kit/metro-config": patch
|
||||
---
|
||||
|
||||
Let users know if they're missing `@react-native/metro-config` when on React
|
||||
Native 0.72+
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@rnx-kit/metro-config": patch
|
||||
---
|
||||
|
||||
Workaround for Expo unintentionally overwriting our Metro config
|
|
@ -24,7 +24,6 @@
|
|||
"test": "rnx-kit-scripts test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rnx-kit/console": "^1.0.0",
|
||||
"@rnx-kit/tools-node": "^2.0.0",
|
||||
"@rnx-kit/tools-react-native": "^1.3.4",
|
||||
"@rnx-kit/tools-workspaces": "^0.1.3"
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
// @ts-check
|
||||
const {
|
||||
readPackage,
|
||||
findPackageDependencyDir,
|
||||
} = require("@rnx-kit/tools-node/package");
|
||||
const { findUp } = require("@rnx-kit/tools-node/path");
|
||||
const { requireModuleFromMetro } = require("@rnx-kit/tools-react-native/metro");
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* @typedef {import("metro-config").MetroConfig} MetroConfig;
|
||||
|
@ -39,6 +42,33 @@ function getPreludeModules(availablePlatforms, projectRoot) {
|
|||
return Array.from(mainModules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we need to build a complete Metro config.
|
||||
*
|
||||
* This is a requirement starting with 0.72 as `@react-native-community/cli`
|
||||
* will no longer provide defaults.
|
||||
*
|
||||
* @param {string} projectRoot
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function needsFullConfig(projectRoot) {
|
||||
const options = { startDir: projectRoot };
|
||||
const pkgJson = findUp("package.json", options);
|
||||
if (!pkgJson) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rnDir = findPackageDependencyDir("react-native", options);
|
||||
if (!rnDir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { version } = readPackage(rnDir);
|
||||
const [major, minor = 0] = version.split(".");
|
||||
const v = Number(major) * 1000 + Number(minor);
|
||||
return v === 0 || v >= 72;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} moduleName
|
||||
* @param {string} implementation
|
||||
|
@ -94,6 +124,22 @@ function outOfTreePlatformResolver(implementations, projectRoot) {
|
|||
return platformResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to resolve `@react-native/metro-config` from specified directory.
|
||||
* @param {string} fromDir
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveMetroConfig(fromDir) {
|
||||
const options = { paths: [fromDir] };
|
||||
try {
|
||||
return require.resolve("@react-native/metro-config", options);
|
||||
} catch (_) {
|
||||
throw new Error(
|
||||
"Cannot find module '@react-native/metro-config'; as of React Native 0.72, it is required for configuring Metro correctly"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns default Metro config.
|
||||
*
|
||||
|
@ -104,41 +150,30 @@ function outOfTreePlatformResolver(implementations, projectRoot) {
|
|||
* @returns {MetroConfig[]}
|
||||
*/
|
||||
function getDefaultConfig(projectRoot) {
|
||||
const pkgJson = findUp("package.json", { startDir: projectRoot });
|
||||
if (!pkgJson) {
|
||||
if (!needsFullConfig(projectRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const manifest = fs.readFileSync(pkgJson, { encoding: "utf-8" });
|
||||
if (manifest.includes("@react-native/metro-config")) {
|
||||
try {
|
||||
const metroConfigPath = require.resolve("@react-native/metro-config", {
|
||||
paths: [projectRoot],
|
||||
});
|
||||
const { getDefaultConfig } = require(metroConfigPath);
|
||||
const { getAvailablePlatforms } = require("@rnx-kit/tools-react-native");
|
||||
const metroConfigPath = resolveMetroConfig(projectRoot);
|
||||
|
||||
const defaultConfig = getDefaultConfig(projectRoot);
|
||||
const { getDefaultConfig } = require(metroConfigPath);
|
||||
const { getAvailablePlatforms } = require("@rnx-kit/tools-react-native");
|
||||
|
||||
const availablePlatforms = getAvailablePlatforms(projectRoot);
|
||||
defaultConfig.resolver.platforms = Object.keys(availablePlatforms);
|
||||
defaultConfig.resolver.resolveRequest = outOfTreePlatformResolver(
|
||||
availablePlatforms,
|
||||
projectRoot
|
||||
);
|
||||
const defaultConfig = getDefaultConfig(projectRoot);
|
||||
|
||||
const preludeModules = getPreludeModules(availablePlatforms, projectRoot);
|
||||
defaultConfig.serializer.getModulesRunBeforeMainModule = () => {
|
||||
return preludeModules;
|
||||
};
|
||||
const availablePlatforms = getAvailablePlatforms(projectRoot);
|
||||
defaultConfig.resolver.platforms = Object.keys(availablePlatforms);
|
||||
defaultConfig.resolver.resolveRequest = outOfTreePlatformResolver(
|
||||
availablePlatforms,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
return [defaultConfig];
|
||||
} catch (_) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
const preludeModules = getPreludeModules(availablePlatforms, projectRoot);
|
||||
defaultConfig.serializer.getModulesRunBeforeMainModule = () => {
|
||||
return preludeModules;
|
||||
};
|
||||
|
||||
return [];
|
||||
return [defaultConfig];
|
||||
}
|
||||
|
||||
exports.getDefaultConfig = getDefaultConfig;
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// @ts-check
|
||||
|
||||
/** @typedef {import("metro-config").MetroConfig} MetroConfig */
|
||||
|
||||
/**
|
||||
* Determines whether this is an Expo config.
|
||||
* @param {MetroConfig=} config
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isExpoConfig(config) {
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://github.com/expo/expo/blob/sdk-51/packages/%40expo/metro-config/src/ExpoMetroConfig.ts#L256
|
||||
const transformer = config.transformer;
|
||||
if (transformer) {
|
||||
if (
|
||||
"_expoRelativeProjectRoot" in transformer ||
|
||||
transformer.babelTransformerPath?.includes("@expo")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return Boolean(config.transformerPath?.includes("@expo"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies workarounds to make the Expo config work.
|
||||
* @param {MetroConfig} config
|
||||
* @param {MetroConfig} defaultConfig
|
||||
* @returns {void}
|
||||
*/
|
||||
function applyExpoWorkarounds(config, defaultConfig) {
|
||||
// Expo's default config is based on Metro's default config, and Metro's
|
||||
// default config sets some fields (like `resolveRequest`) to `null`, which
|
||||
// then overwrites our fields:
|
||||
// https://github.com/facebook/metro/blob/v0.80.10/packages/metro-config/src/defaults/index.js#L51
|
||||
if (config.resolver?.resolveRequest === null) {
|
||||
delete config.resolver.resolveRequest;
|
||||
}
|
||||
|
||||
// Expo _always_ sets `getModulesRunBeforeMainModule`:
|
||||
// https://github.com/expo/expo/blob/sdk-51/packages/%40expo/metro-config/src/ExpoMetroConfig.ts#L207
|
||||
const getModulesRunBeforeMainModule =
|
||||
config.serializer?.getModulesRunBeforeMainModule;
|
||||
if (getModulesRunBeforeMainModule) {
|
||||
const core = /Libraries[/\\]Core[/\\]InitializeCore/;
|
||||
const prelude =
|
||||
defaultConfig.serializer?.getModulesRunBeforeMainModule?.("") ?? [];
|
||||
config.serializer.getModulesRunBeforeMainModule = (entryFilePath) => {
|
||||
const modules = prelude.slice();
|
||||
for (const m of getModulesRunBeforeMainModule(entryFilePath)) {
|
||||
if (!core.test(m)) {
|
||||
modules.push(m);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exports.applyExpoWorkarounds = applyExpoWorkarounds;
|
||||
exports.isExpoConfig = isExpoConfig;
|
|
@ -7,8 +7,10 @@ const {
|
|||
} = require("@rnx-kit/tools-react-native/metro");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { applyExpoWorkarounds, isExpoConfig } = require("./expoConfig");
|
||||
|
||||
/**
|
||||
* @typedef {import("metro-config").InputConfigT} InputConfigT;
|
||||
* @typedef {import("metro-config").MetroConfig} MetroConfig;
|
||||
*/
|
||||
|
||||
|
@ -261,11 +263,11 @@ module.exports = {
|
|||
|
||||
/**
|
||||
* Helper function for configuring Metro.
|
||||
* @param {MetroConfig=} customConfig
|
||||
* @param {MetroConfig=} inputConfig
|
||||
* @returns {MetroConfig}
|
||||
*/
|
||||
makeMetroConfig: (customConfig = {}) => {
|
||||
const projectRoot = customConfig.projectRoot || process.cwd();
|
||||
makeMetroConfig: (inputConfig = {}) => {
|
||||
const projectRoot = inputConfig.projectRoot || process.cwd();
|
||||
|
||||
const { mergeConfig } = requireModuleFromMetro("metro-config", projectRoot);
|
||||
const { enhanceMiddleware } = require("./assetPluginForMonorepos");
|
||||
|
@ -273,9 +275,10 @@ module.exports = {
|
|||
|
||||
const blockList = exclusionList([], projectRoot);
|
||||
const customBlockList =
|
||||
customConfig.resolver &&
|
||||
(customConfig.resolver.blockList || customConfig.resolver.blacklistRE);
|
||||
inputConfig.resolver &&
|
||||
(inputConfig.resolver.blockList || inputConfig.resolver.blacklistRE);
|
||||
|
||||
/** @type {InputConfigT[]} */
|
||||
const [defaultConfig, ...configs] = [
|
||||
...getDefaultConfig(projectRoot),
|
||||
{
|
||||
|
@ -295,12 +298,12 @@ module.exports = {
|
|||
},
|
||||
}),
|
||||
},
|
||||
watchFolders: customConfig.watchFolders ?? defaultWatchFolders(),
|
||||
watchFolders: inputConfig.watchFolders ?? defaultWatchFolders(),
|
||||
},
|
||||
{
|
||||
...customConfig,
|
||||
...inputConfig,
|
||||
resolver: {
|
||||
...customConfig.resolver,
|
||||
...inputConfig.resolver,
|
||||
...(customBlockList
|
||||
? {
|
||||
// Metro introduced `blockList` in 0.60, but still prefers
|
||||
|
@ -317,13 +320,17 @@ module.exports = {
|
|||
* @see exclusionList for further information.
|
||||
*/
|
||||
...resolveUniqueModules(UNIQUE_MODULES, projectRoot),
|
||||
...(customConfig.resolver
|
||||
? customConfig.resolver.extraNodeModules
|
||||
: {}),
|
||||
...inputConfig.resolver?.extraNodeModules,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const userConfig = configs[configs.length - 1];
|
||||
if (isExpoConfig(userConfig)) {
|
||||
applyExpoWorkarounds(userConfig, defaultConfig);
|
||||
}
|
||||
|
||||
return mergeConfig(defaultConfig, ...configs);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import type { MetroConfig } from "metro-config";
|
||||
import { deepEqual, ok } from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { applyExpoWorkarounds, isExpoConfig } from "../src/expoConfig";
|
||||
|
||||
describe("isExpoConfig()", () => {
|
||||
it("returns true when it's likely a config comes from Expo", () => {
|
||||
ok(!isExpoConfig());
|
||||
ok(!isExpoConfig({}));
|
||||
ok(
|
||||
!isExpoConfig({
|
||||
transformer: { babelTransformerPath: "metro-babel-transformer" },
|
||||
})
|
||||
);
|
||||
ok(!isExpoConfig({ transformerPath: "metro-transform-worker" }));
|
||||
|
||||
ok(
|
||||
isExpoConfig({
|
||||
transformer: { _expoRelativeProjectRoot: null },
|
||||
} as MetroConfig)
|
||||
);
|
||||
ok(isExpoConfig({ transformer: { babelTransformerPath: "@expo" } }));
|
||||
ok(isExpoConfig({ transformerPath: "@expo" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyExpoWorkarounds()", () => {
|
||||
it("removes `config.resolver.resolveRequest` when it's `null`", () => {
|
||||
const config = { resolver: { resolveRequest: null } };
|
||||
|
||||
ok("resolveRequest" in config.resolver);
|
||||
|
||||
applyExpoWorkarounds(config, {});
|
||||
|
||||
ok(!("resolveRequest" in config.resolver));
|
||||
});
|
||||
|
||||
it("replaces `config.serializer.getModulesRunBeforeMainModule`", () => {
|
||||
const expoConfig = {
|
||||
serializer: {
|
||||
getModulesRunBeforeMainModule: () => [
|
||||
"react-native/Libraries/Core/InitializeCore.js",
|
||||
"expo/build/winter",
|
||||
"@expo/metro-runtime",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const defaultConfig = {
|
||||
serializer: {
|
||||
getModulesRunBeforeMainModule: () => [
|
||||
"react-native/Libraries/Core/InitializeCore.js",
|
||||
"react-native-macos/Libraries/Core/InitializeCore.js",
|
||||
"react-native-windows/Libraries/Core/InitializeCore.js",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
applyExpoWorkarounds(expoConfig, defaultConfig);
|
||||
|
||||
deepEqual(expoConfig.serializer.getModulesRunBeforeMainModule(), [
|
||||
"react-native/Libraries/Core/InitializeCore.js",
|
||||
"react-native-macos/Libraries/Core/InitializeCore.js",
|
||||
"react-native-windows/Libraries/Core/InitializeCore.js",
|
||||
"expo/build/winter",
|
||||
"@expo/metro-runtime",
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -405,6 +405,7 @@ describe("makeMetroConfig", () => {
|
|||
|
||||
it("sets both `blacklistRE` and `blockList`", () => {
|
||||
const configWithBlacklist = makeMetroConfig({
|
||||
projectRoot,
|
||||
resolver: {
|
||||
blacklistRE: /.*/,
|
||||
},
|
||||
|
@ -415,6 +416,7 @@ describe("makeMetroConfig", () => {
|
|||
equal(blacklistRE, configWithBlacklist.resolver?.blockList);
|
||||
|
||||
const configWithBlockList = makeMetroConfig({
|
||||
projectRoot,
|
||||
resolver: {
|
||||
blockList: /.*/,
|
||||
},
|
||||
|
|
|
@ -4008,7 +4008,6 @@ __metadata:
|
|||
dependencies:
|
||||
"@babel/core": "npm:^7.20.0"
|
||||
"@babel/preset-env": "npm:^7.20.0"
|
||||
"@rnx-kit/console": "npm:^1.0.0"
|
||||
"@rnx-kit/eslint-config": "npm:*"
|
||||
"@rnx-kit/scripts": "npm:*"
|
||||
"@rnx-kit/tools-node": "npm:^2.0.0"
|
||||
|
|
Загрузка…
Ссылка в новой задаче