Skip to content

Commit 89e9761

Browse files
authored
fix: transform metro require (#85)
1 parent ce92bfe commit 89e9761

File tree

14 files changed

+207
-185
lines changed

14 files changed

+207
-185
lines changed

apps/example-host/metro.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module.exports = withModuleFederation(
5252
{
5353
flags: {
5454
unstable_patchHMRClient: true,
55+
unstable_patchRuntimeRequire: true,
5556
},
5657
}
5758
);

apps/example-mini/metro.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ module.exports = withModuleFederation(
5151
{
5252
flags: {
5353
unstable_patchHMRClient: true,
54+
unstable_patchRuntimeRequire: true,
5455
},
5556
}
5657
);

apps/example-nested-mini/metro.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ module.exports = withModuleFederation(
5555
{
5656
flags: {
5757
unstable_patchHMRClient: true,
58+
unstable_patchRuntimeRequire: true,
5859
},
5960
}
6061
);

apps/showcase-host/metro.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ module.exports = withModuleFederation(
5757
{
5858
flags: {
5959
unstable_patchHMRClient: true,
60+
unstable_patchRuntimeRequire: true,
6061
},
6162
}
6263
);

apps/showcase-mini/metro.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ module.exports = withModuleFederation(
6060
{
6161
flags: {
6262
unstable_patchHMRClient: true,
63+
unstable_patchRuntimeRequire: true,
6364
},
6465
}
6566
);
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// reuse `@babel/types` from `metro`
2+
const metroPath = require.resolve('metro');
3+
const babelTypesPath = require.resolve('@babel/types', { paths: [metroPath] });
4+
const t = require(babelTypesPath);
5+
6+
const METRO_PREFIX = '__METRO_GLOBAL_PREFIX__';
7+
const GLOBAL_NAMES_TO_PREFIX = ['__r', '__c', '__registerSegment', '__accept'];
8+
const REFRESH_SYMBOLS = ['$RefreshReg$', '$RefreshSig$'];
9+
10+
/**
11+
* - global.__r = metroRequire;
12+
* + global[`${__METRO_GLOBAL_PREFIX__}__r`] = metroRequire;
13+
*/
14+
function prefixGlobalNames(path) {
15+
const { object, property, computed } = path.node;
16+
17+
if (
18+
t.isIdentifier(object, { name: 'global' }) &&
19+
t.isIdentifier(property) &&
20+
!computed &&
21+
GLOBAL_NAMES_TO_PREFIX.includes(property.name)
22+
) {
23+
path.replaceWith(
24+
t.memberExpression(
25+
t.identifier('global'),
26+
t.templateLiteral(
27+
[
28+
t.templateElement({ raw: '', cooked: '' }),
29+
t.templateElement(
30+
{ raw: property.name, cooked: property.name },
31+
true
32+
),
33+
],
34+
[t.identifier(METRO_PREFIX)]
35+
),
36+
true
37+
)
38+
);
39+
}
40+
}
41+
42+
/**
43+
* - global.$RefreshReg$ = () => {};
44+
* - global.$RefreshSig$ = () => (type) => type;
45+
* + global.$RefreshReg$ = global.$RefreshReg$ ?? (() => {});
46+
* + global.$RefreshSig$ = global.$RefreshSig$ ?? (() => type => type);
47+
*/
48+
function defaultRefreshSymbols(path, state) {
49+
const { node } = path;
50+
51+
if (
52+
t.isMemberExpression(node.left) &&
53+
t.isIdentifier(node.left.object, { name: 'global' }) &&
54+
t.isIdentifier(node.left.property) &&
55+
REFRESH_SYMBOLS.includes(node.left.property.name)
56+
) {
57+
const propName = node.left.property.name;
58+
59+
if (state.hasTransformed[propName]) {
60+
return;
61+
}
62+
63+
const globalProp = t.memberExpression(
64+
t.identifier('global'),
65+
t.identifier(node.left.property.name)
66+
);
67+
68+
path.replaceWith(
69+
t.assignmentExpression(
70+
'=',
71+
globalProp,
72+
t.logicalExpression('??', globalProp, node.right)
73+
)
74+
);
75+
state.hasTransformed[propName] = true;
76+
}
77+
}
78+
79+
/**
80+
* - RefreshRuntime.register(type, moduleId + " " + id);
81+
* + RefreshRuntime.register(type, __METRO_GLOBAL_PREFIX__ + ' ' + moduleId + ' ' + id);
82+
*/
83+
function prefixModuleIDs(path) {
84+
const { callee, arguments: args } = path.node;
85+
const isRegisterExports = t.isIdentifier(callee, {
86+
name: 'registerExportsForReactRefresh',
87+
});
88+
const isRefreshRuntimeRegister =
89+
t.isMemberExpression(callee) &&
90+
t.isIdentifier(callee.object, { name: 'RefreshRuntime' }) &&
91+
t.isIdentifier(callee.property, { name: 'register' });
92+
93+
if (!isRegisterExports && !isRefreshRuntimeRegister) return;
94+
95+
const lastArgIndex = args.length - 1;
96+
const lastArg = args[lastArgIndex];
97+
98+
args[lastArgIndex] = t.binaryExpression(
99+
'+',
100+
t.identifier(METRO_PREFIX),
101+
t.binaryExpression('+', t.stringLiteral(' '), lastArg)
102+
);
103+
}
104+
105+
/**
106+
* - global[__METRO_GLOBAL_PREFIX__ + '__ReactRefresh'] || metroRequire.Refresh
107+
* + global[global.__METRO_GLOBAL_PREFIX__ + '__ReactRefresh'] ||
108+
* + metroRequire.Refresh
109+
*/
110+
function patchRequireRefresh(path) {
111+
const funcPath = path.findParent(
112+
(p) =>
113+
(p.isFunctionDeclaration() || p.isFunctionExpression()) &&
114+
p.node.id?.name === 'requireRefresh'
115+
);
116+
117+
if (!funcPath) return;
118+
119+
const newReturn = t.logicalExpression(
120+
'||',
121+
t.memberExpression(
122+
t.identifier('global'),
123+
t.binaryExpression(
124+
'+',
125+
t.memberExpression(t.identifier('global'), t.identifier(METRO_PREFIX)),
126+
t.stringLiteral('__ReactRefresh')
127+
),
128+
true
129+
),
130+
t.memberExpression(t.identifier('metroRequire'), t.identifier('Refresh'))
131+
);
132+
133+
path.get('argument').replaceWith(newReturn);
134+
}
135+
136+
function metroPatchRequireBabelPlugin() {
137+
return {
138+
name: 'metro-patch-require',
139+
visitor: {
140+
Program: {
141+
enter(_, state) {
142+
// Transform only require.js from metro-runtime
143+
state.shouldTransform = state.file.opts.filename.includes(
144+
'metro-runtime/src/polyfills/require.js'
145+
);
146+
// Perform refreshSymbols transformation only once
147+
// Because it is referenced in multiple places
148+
state.hasTransformed = {
149+
$RefreshReg$: false,
150+
$RefreshSig$: false,
151+
};
152+
},
153+
},
154+
MemberExpression(path, state) {
155+
if (!state.shouldTransform) return;
156+
prefixGlobalNames(path);
157+
},
158+
AssignmentExpression(path, state) {
159+
if (!state.shouldTransform) return;
160+
defaultRefreshSymbols(path, state);
161+
},
162+
CallExpression(path, state) {
163+
if (!state.shouldTransform) return;
164+
prefixModuleIDs(path);
165+
},
166+
ReturnStatement(path, state) {
167+
if (!state.shouldTransform) return;
168+
patchRequireRefresh(path);
169+
},
170+
},
171+
};
172+
}
173+
174+
module.exports = metroPatchRequireBabelPlugin;

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"default": "./dist/commands/index.js"
3636
},
3737
"./babel-plugin": "./babel-plugin/index.js",
38+
"./babel-plugin/patch-require": "./babel-plugin/patch-require.js",
3839
"./bootstrap": {
3940
"dev": "./src/modules/asyncStartup.tsx",
4041
"default": "./dist/modules/asyncStartup.tsx"

packages/core/src/babel/transformer.js

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,9 @@
1-
const blacklistedPaths = __BLACKLISTED_PATHS__;
2-
const remotes = __REMOTES__;
3-
const shared = __SHARED__;
4-
51
const babelTransformer = require('__BABEL_TRANSFORMER_PATH__');
62

73
function transform(config) {
8-
const federationPlugins = [
9-
[
10-
'@module-federation/metro/babel-plugin',
11-
{ blacklistedPaths, remotes, shared },
12-
],
13-
];
14-
154
return babelTransformer.transform({
165
...config,
17-
plugins: [...federationPlugins, ...config.plugins],
6+
plugins: [...__BABEL_PLUGINS__, ...config.plugins],
187
});
198
}
209

packages/core/src/plugin/babel-transformer.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,37 @@ interface CreateBabelTransformerOptions {
77
federationConfig: ModuleFederationConfigNormalized;
88
originalBabelTransformerPath: string;
99
tmpDirPath: string;
10+
enableRuntimeRequirePatching: boolean;
1011
}
1112

1213
export function createBabelTransformer({
1314
blacklistedPaths,
1415
federationConfig,
1516
originalBabelTransformerPath,
1617
tmpDirPath,
18+
enableRuntimeRequirePatching,
1719
}: CreateBabelTransformerOptions) {
1820
const outputPath = path.join(tmpDirPath, 'babel-transformer.js');
1921
const templatePath = require.resolve('../babel/transformer.js');
2022
const transformerTemplate = fs.readFileSync(templatePath, 'utf-8');
2123

24+
const plugins = [
25+
[
26+
'@module-federation/metro/babel-plugin',
27+
{
28+
blacklistedPaths,
29+
remotes: federationConfig.remotes,
30+
shared: federationConfig.shared,
31+
},
32+
],
33+
enableRuntimeRequirePatching
34+
? '@module-federation/metro/babel-plugin/patch-require'
35+
: undefined,
36+
].filter(Boolean);
37+
2238
const babelTransformer = transformerTemplate
2339
.replaceAll('__BABEL_TRANSFORMER_PATH__', originalBabelTransformerPath)
24-
.replaceAll('__REMOTES__', JSON.stringify(federationConfig.remotes))
25-
.replaceAll('__SHARED__', JSON.stringify(federationConfig.shared))
26-
.replaceAll('__BLACKLISTED_PATHS__', JSON.stringify(blacklistedPaths));
40+
.replaceAll('__BABEL_PLUGINS__', JSON.stringify(plugins));
2741

2842
fs.writeFileSync(outputPath, babelTransformer, 'utf-8');
2943

packages/core/src/plugin/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ function augmentConfig(
104104
federationConfig: options,
105105
originalBabelTransformerPath: config.transformer.babelTransformerPath,
106106
tmpDirPath: tmpDirPath,
107+
enableRuntimeRequirePatching: Boolean(
108+
extraOptions?.flags?.unstable_patchRuntimeRequire
109+
),
107110
});
108111

109112
const manifestPath = createManifest(options, tmpDirPath);

0 commit comments

Comments
 (0)