Skip to content

Commit ed0e9ae

Browse files
author
theGrep01
committed
feat: adds metro-core, metro-plugin-rnc-cli, metro-plugin-rnef packages
1 parent 7dbc25d commit ed0e9ae

File tree

91 files changed

+5424
-1279
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+5424
-1279
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@
213213
"tailwindcss": "3.4.13",
214214
"terser-webpack-plugin": "^5.3.10",
215215
"ts-jest": "29.1.5",
216+
"ts-node": "10.9.1",
216217
"tslib": "2.8.1",
217218
"tsup": "7.3.0",
218219
"typescript": "5.8.3",

packages/metro-core/.eslintrc.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"extends": ["../../.eslintrc.json"],
3+
"ignorePatterns": [
4+
"!**/*",
5+
"**/*.d.ts",
6+
"**/vite.config.*.timestamp*",
7+
"**/vitest.config.*.timestamp*"
8+
],
9+
"overrides": [
10+
{
11+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
12+
"rules": {
13+
"@typescript-eslint/no-explicit-any": "off",
14+
"@typescript-eslint/ban-ts-comment": "warn",
15+
"@typescript-eslint/no-var-requires": 0,
16+
"@typescript-eslint/no-restricted-imports": [
17+
"error",
18+
{
19+
"paths": [
20+
{
21+
"name": "webpack",
22+
"message": "Please use require(normalizeWebpackPath('webpack')) instead.",
23+
"allowTypeImports": true
24+
}
25+
],
26+
"patterns": [
27+
{
28+
"group": ["webpack/lib/*"],
29+
"message": "Please use require(normalizeWebpackPath('webpack')) instead.",
30+
"allowTypeImports": true
31+
}
32+
]
33+
}
34+
]
35+
}
36+
},
37+
{
38+
"files": ["*.js", "*.jsx"]
39+
}
40+
]
41+
}

packages/metro-core/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Callstack and Zephyr Cloud
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

packages/metro-core/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# metro
2+
3+
This library was generated with [Nx](https://nx.dev).
4+
5+
## Building
6+
7+
Run `nx build metro` to build the library.
8+
9+
## Running unit tests
10+
11+
Run `nx test metro` to execute the unit tests via [Jest](https://jestjs.io).
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 UNSUPPORTED_IMPORT_MESSAGE =
7+
'The module path for import() must be a static string literal. Expressions or variables are not supported.';
8+
const WEBPACK_IGNORE_COMMENT = 'webpackIgnore: true';
9+
10+
function isIgnoredWebpackImport(path) {
11+
const [firstArg] = path.node.arguments;
12+
13+
const comments = [
14+
...(firstArg?.leadingComments || []),
15+
...(path.node?.leadingComments || []),
16+
...(path.node?.innerComments || []),
17+
];
18+
19+
return (
20+
t.isImport(path.node.callee) &&
21+
comments.some((comment) => comment.value.includes(WEBPACK_IGNORE_COMMENT))
22+
);
23+
}
24+
25+
function getRemotesRegExp(remotes) {
26+
return new RegExp(`^(${Object.keys(remotes).join('|')})/`);
27+
}
28+
29+
function getSharedRegExp(shared) {
30+
return new RegExp(`^(${Object.keys(shared).join('|')})$`);
31+
}
32+
33+
function isRemoteImport(path, options) {
34+
return (
35+
t.isImport(path.node.callee) &&
36+
t.isStringLiteral(path.node.arguments[0]) &&
37+
Object.keys(options.remotes).length > 0 &&
38+
path.node.arguments[0].value.match(getRemotesRegExp(options.remotes))
39+
);
40+
}
41+
42+
function isSharedImport(path, options) {
43+
return (
44+
t.isImport(path.node.callee) &&
45+
t.isStringLiteral(path.node.arguments[0]) &&
46+
Object.keys(options.shared).length > 0 &&
47+
path.node.arguments[0].value.match(getSharedRegExp(options.shared))
48+
);
49+
}
50+
51+
function createWrappedImport(importName, methodName) {
52+
const importArg = t.stringLiteral(importName);
53+
54+
// require('mf:remote-module-registry')
55+
const requireCall = t.callExpression(t.identifier('require'), [
56+
t.stringLiteral('mf:remote-module-registry'),
57+
]);
58+
59+
// .loadAndGetRemote(importName) or .loadAndGetShared(importName)
60+
const loadAndGetCall = t.callExpression(
61+
t.memberExpression(requireCall, t.identifier(methodName)),
62+
[importArg],
63+
);
64+
65+
return loadAndGetCall;
66+
}
67+
68+
function getWrappedRemoteImport(importName) {
69+
return createWrappedImport(importName, 'loadAndGetRemote');
70+
}
71+
72+
function getWrappedSharedImport(importName) {
73+
return createWrappedImport(importName, 'loadAndGetShared');
74+
}
75+
76+
function getRejectedPromise(errorMessage) {
77+
return t.callExpression(
78+
t.memberExpression(t.identifier('Promise'), t.identifier('reject')),
79+
[t.newExpression(t.identifier('Error'), [t.stringLiteral(errorMessage)])],
80+
);
81+
}
82+
83+
function moduleFederationMetroBabelPlugin() {
84+
return {
85+
name: 'module-federation-metro-babel-plugin',
86+
visitor: {
87+
CallExpression(path, state) {
88+
if (state.opts.blacklistedPaths.includes(state.filename)) {
89+
return;
90+
}
91+
92+
// Workaround to remove problematic import from `loadEsmEntry` in `@module-federation/runtime-core`
93+
// That causes crashes in Metro bundler
94+
if (isIgnoredWebpackImport(path)) {
95+
path.replaceWith(getRejectedPromise(UNSUPPORTED_IMPORT_MESSAGE));
96+
return;
97+
}
98+
99+
if (isRemoteImport(path, state.opts)) {
100+
const wrappedImport = getWrappedRemoteImport(
101+
path.node.arguments[0].value,
102+
);
103+
path.replaceWith(wrappedImport);
104+
} else if (isSharedImport(path, state.opts)) {
105+
const wrappedImport = getWrappedSharedImport(
106+
path.node.arguments[0].value,
107+
);
108+
path.replaceWith(wrappedImport);
109+
}
110+
},
111+
},
112+
};
113+
}
114+
115+
module.exports = moduleFederationMetroBabelPlugin;
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
/**
7+
* Inject require('mf:init-host') right after 'use strict' directive
8+
* in React Native's Initialize Core
9+
*/
10+
function injectInitHostRequire(path, state) {
11+
// Only process once per file
12+
if (state.hasInjected) return;
13+
14+
let insertIndex = 0;
15+
16+
// Find the position after 'use strict' directive
17+
for (let i = 0; i < path.node.body.length; i++) {
18+
const node = path.node.body[i];
19+
if (
20+
t.isExpressionStatement(node) &&
21+
t.isStringLiteral(node.expression) &&
22+
node.expression.value === 'use strict'
23+
) {
24+
insertIndex = i + 1;
25+
break;
26+
}
27+
}
28+
29+
// Create the require('mf:init-host') statement
30+
const requireStatement = t.expressionStatement(
31+
t.callExpression(t.identifier('require'), [
32+
t.stringLiteral('mf:init-host'),
33+
]),
34+
);
35+
36+
// Insert the require statement
37+
path.node.body.splice(insertIndex, 0, requireStatement);
38+
state.hasInjected = true;
39+
}
40+
41+
function metroPatchInitializeCorePlugin() {
42+
return {
43+
name: 'module-federation-metro-patch-initialize-core',
44+
visitor: {
45+
Program: {
46+
enter(_, state) {
47+
state.hasInjected = false;
48+
state.shouldTransform = state.file.opts.filename.includes(
49+
'react-native/Libraries/Core/InitializeCore.js',
50+
);
51+
},
52+
exit(path, state) {
53+
if (!state.shouldTransform) return;
54+
injectInitHostRequire(path, state);
55+
},
56+
},
57+
},
58+
};
59+
}
60+
61+
module.exports = metroPatchInitializeCorePlugin;

0 commit comments

Comments
 (0)