Skip to content

Commit 600e838

Browse files
committed
Upgrade ERN to support RN 0.81
1 parent 94d2faf commit 600e838

54 files changed

Lines changed: 1162 additions & 166 deletions

File tree

Some content is hidden

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

azure/templates/system-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
steps:
22
- task: NodeTool@0
33
inputs:
4-
versionSpec: '18.x'
4+
versionSpec: '20.x'
55
displayName: 'Install Node.js'
66
- script: |
77
git config --global user.email "electrodenative@gmail.com"

azure/templates/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
steps:
22
- task: NodeTool@0
33
inputs:
4-
versionSpec: '18.x'
4+
versionSpec: '20.x'
55
displayName: 'Install Node.js'
66
- script: |
77
yarn --frozen-lockfile

ern-composite-gen/src/addRNDepToPjson.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,22 @@ export async function addRNDepToPjson(dir: string, version: string) {
77

88
// For React Native 0.77+, also add required dependencies
99
// This is required by the new Metro config format and CLI
10-
if (semver.gte(version, '0.77.0')) {
10+
if (semver.gte(version, '0.81.0')) {
11+
compositePackageJson.dependencies['@react-native/metro-config'] = version;
12+
compositePackageJson.dependencies['@react-native/babel-preset'] = version;
13+
// RN 0.81+ uses @react-native/community-cli-plugin (internal to RN monorepo)
14+
compositePackageJson.dependencies[
15+
'@react-native/community-cli-plugin'
16+
] = version;
17+
// Add CLI packages to ensure android and ios platforms are available
18+
compositePackageJson.dependencies['@react-native-community/cli'] = '15.1.3';
19+
compositePackageJson.dependencies[
20+
'@react-native-community/cli-platform-android'
21+
] = '15.1.3';
22+
compositePackageJson.dependencies[
23+
'@react-native-community/cli-platform-ios'
24+
] = '15.1.3';
25+
} else if (semver.gte(version, '0.77.0')) {
1126
compositePackageJson.dependencies['@react-native/metro-config'] = version;
1227
compositePackageJson.dependencies['@react-native/babel-preset'] = version;
1328
// Add CLI packages to ensure android and ios platforms are available

ern-composite-gen/src/createMetroConfig.ts

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,61 @@ import fs from 'fs-extra';
22
import path from 'path';
33
import beautify from 'js-beautify';
44
import os from 'os';
5+
import semver from 'semver';
6+
import { getMetroBlacklistPath } from 'ern-core';
57

68
export async function createMetroConfig({
79
cwd,
810
projectRoot,
911
blacklistRe,
1012
extraNodeModules,
1113
watchFolders,
14+
reactNativeVersion,
1215
}: {
1316
cwd?: string;
1417
projectRoot?: string;
1518
blacklistRe?: RegExp[];
1619
extraNodeModules?: { [pkg: string]: string };
1720
watchFolders?: string[];
21+
reactNativeVersion?: string;
1822
}) {
19-
// Metro config format for React Native 0.73+
20-
const metroConfigContent = `const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
23+
const useModernConfig =
24+
reactNativeVersion && semver.gte(reactNativeVersion, '0.73.0');
25+
26+
const metroConfigContent = useModernConfig
27+
? createModernMetroConfig({
28+
projectRoot,
29+
blacklistRe,
30+
extraNodeModules,
31+
watchFolders,
32+
})
33+
: createLegacyMetroConfig({
34+
projectRoot,
35+
blacklistRe,
36+
extraNodeModules,
37+
watchFolders,
38+
reactNativeVersion: reactNativeVersion || '0.60.0',
39+
});
40+
41+
return fs.writeFile(
42+
path.join(cwd ?? path.resolve(), 'metro.config.js'),
43+
beautify.js(metroConfigContent),
44+
);
45+
}
46+
47+
// Metro config format for React Native 0.73+
48+
function createModernMetroConfig({
49+
projectRoot,
50+
blacklistRe,
51+
extraNodeModules,
52+
watchFolders,
53+
}: {
54+
projectRoot?: string;
55+
blacklistRe?: RegExp[];
56+
extraNodeModules?: { [pkg: string]: string };
57+
watchFolders?: string[];
58+
}): string {
59+
return `const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
2160
const defaultConfig = getDefaultConfig(__dirname);
2261
2362
const config = {
@@ -45,7 +84,7 @@ const config = {
4584
],
4685
sourceExts: [
4786
...defaultConfig.resolver.sourceExts,
48-
"svg",
87+
"svg",
4988
"mjs"
5089
],
5190
blockList: [
@@ -72,9 +111,93 @@ const config = {
72111
};
73112
74113
module.exports = mergeConfig(defaultConfig, config);`;
114+
}
75115

76-
return fs.writeFile(
77-
path.join(cwd ?? path.resolve(), 'metro.config.js'),
78-
beautify.js(metroConfigContent),
79-
);
116+
// Legacy Metro config format for React Native < 0.73
117+
function createLegacyMetroConfig({
118+
projectRoot,
119+
blacklistRe,
120+
extraNodeModules,
121+
watchFolders,
122+
reactNativeVersion,
123+
}: {
124+
projectRoot?: string;
125+
blacklistRe?: RegExp[];
126+
extraNodeModules?: { [pkg: string]: string };
127+
watchFolders?: string[];
128+
reactNativeVersion: string;
129+
}): string {
130+
return `const blacklist = require('${getMetroBlacklistPath(reactNativeVersion)}');
131+
module.exports = {
132+
${projectRoot ? `projectRoot: "${projectRoot}",` : ''}
133+
${
134+
watchFolders
135+
? `watchFolders: [
136+
${watchFolders
137+
.map((x) => `"${x.replace(/\\/g, '\\\\')}"`)
138+
.join(`,${os.EOL}`)}
139+
],`
140+
: ''
141+
}
142+
resolver: {
143+
blacklistRE: blacklist([
144+
// Ignore IntelliJ directories
145+
/.*\\.idea\\/.*/,
146+
// ignore git directories
147+
/.*\\.git\\/.*/,
148+
// Ignore android directories
149+
/.*\\/app\\/build\\/.*/,
150+
${blacklistRe ? blacklistRe.join(`,${os.EOL}`) : ''}
151+
]),
152+
${
153+
extraNodeModules
154+
? `extraNodeModules: ${JSON.stringify(extraNodeModules, null, 2)},`
155+
: ''
156+
}
157+
assetExts: [
158+
// Image formats
159+
"bmp",
160+
"gif",
161+
"jpg",
162+
"jpeg",
163+
"png",
164+
"psd",
165+
"webp",
166+
// Video formats
167+
"m4v",
168+
"mov",
169+
"mp4",
170+
"mpeg",
171+
"mpg",
172+
"webm",
173+
// Audio formats
174+
"aac",
175+
"aiff",
176+
"caf",
177+
"m4a",
178+
"mp3",
179+
"wav",
180+
// Document formats
181+
"html",
182+
"pdf",
183+
// Font formats
184+
"otf",
185+
"ttf",
186+
// Archives (virtual files)
187+
"zip"
188+
],
189+
sourceExts: ["js", "json", "ts", "tsx", "svg", "mjs"],
190+
},
191+
transformer: {
192+
getTransformOptions: async () => ({
193+
transform: {
194+
experimentalImportSupport: false,
195+
inlineRequires: false,
196+
},
197+
}),
198+
assetPlugins: ['ern-bundle-store-metro-asset-plugin'],
199+
babelTransformerPath: require.resolve("react-native-svg-transformer"),
200+
},
201+
};
202+
`;
80203
}

ern-composite-gen/src/generateComposite.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ You should resolve the following version mismatches prior to retrying.${os.EOL}`
330330
blacklistRe,
331331
cwd: outDir,
332332
extraNodeModules,
333+
reactNativeVersion: rnVersion,
333334
watchFolders: localMiniAppsPaths,
334335
});
335336
if (semver.gte(rnVersion, '0.57.0')) {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
44
networkTimeout=10000
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

ern-container-gen/src/reactNativeBundleAndroid.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export async function reactNativeBundleAndroid({
4343
platform: 'android',
4444
resetCache,
4545
sourceMapOutput,
46+
workingDir: cwd,
4647
});
4748
return result;
4849
} finally {

ern-container-gen/src/reactNativeBundleIos.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export async function reactNativeBundleIos({
5656
platform: 'ios',
5757
resetCache,
5858
sourceMapOutput,
59+
workingDir: cwd,
5960
});
6061
return result;
6162
} finally {

ern-core/src/ReactNativeCli.ts

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import log from './log';
99
import kax from './kax';
1010
import util from 'util';
1111
import semver from 'semver';
12-
import os from 'os';
1312

1413
const ex = util.promisify(exec);
1514
const sp = util.promisify(spawn);
@@ -89,7 +88,9 @@ export default class ReactNativeCli {
8988
}
9089
const initCmd = `init ${projectName} ${options.join(' ')}`;
9190

92-
if (semver.gte(rnVersion, '0.77.0')) {
91+
if (semver.gte(rnVersion, '0.81.0')) {
92+
return execp(`npx @react-native-community/cli@15.1.3 ${initCmd}`);
93+
} else if (semver.gte(rnVersion, '0.77.0')) {
9394
return execp(`npx @react-native-community/cli@15.0.1 ${initCmd}`);
9495
} else if (semver.gte(rnVersion, '0.60.0')) {
9596
return execp(
@@ -123,28 +124,36 @@ export default class ReactNativeCli {
123124
const shouldUseCommunityCliForProject =
124125
await this.shouldUseCommunityCliForProject(workingDir);
125126

126-
let bundleCommand: string;
127-
if (shouldUseCommunityCliForProject) {
128-
bundleCommand = `npx @react-native-community/cli bundle \
127+
const bundleArgs = `\
129128
${entryFile ? `--entry-file=${entryFile}` : ''} \
130129
${dev ? '--dev=true' : '--dev=false'} \
131130
${platform ? `--platform=${platform}` : ''} \
132131
${bundleOutput ? `--bundle-output=${bundleOutput}` : ''} \
133132
${assetsDest ? `--assets-dest=${assetsDest}` : ''} \
134133
${sourceMapOutput ? `--sourcemap-output=${sourceMapOutput}` : ''} \
135134
${resetCache ? '--reset-cache' : ''}`;
135+
136+
let bundleCommand: string;
137+
const execOptions: any = { cwd: workingDir };
138+
if (shouldUseCommunityCliForProject) {
139+
// RN >= 0.77: Use npx @react-native-community/cli (ships metro plugin via RN itself)
140+
bundleCommand = `npx @react-native-community/cli bundle ${bundleArgs}`;
136141
} else {
137-
bundleCommand = `${this.binaryPath} bundle \
138-
${entryFile ? `--entry-file=${entryFile}` : ''} \
139-
${dev ? '--dev=true' : '--dev=false'} \
140-
${platform ? `--platform=${platform}` : ''} \
141-
${bundleOutput ? `--bundle-output=${bundleOutput}` : ''} \
142-
${assetsDest ? `--assets-dest=${assetsDest}` : ''} \
143-
${sourceMapOutput ? `--sourcemap-output=${sourceMapOutput}` : ''} \
144-
${resetCache ? '--reset-cache' : ''}`;
142+
// RN < 0.77: Use the project-local react-native CLI from node_modules
143+
// The composite project's react-native dependency ships its own CLI version
144+
// that includes the bundle command (via cli-plugin-metro)
145+
const localCliBin = workingDir
146+
? path.join(workingDir, 'node_modules', '.bin', 'react-native')
147+
: 'react-native';
148+
bundleCommand = `${localCliBin} bundle ${bundleArgs}`;
149+
// Older Metro versions use md4 hash which is unsupported in Node 17+ (OpenSSL 3)
150+
execOptions.env = {
151+
...process.env,
152+
NODE_OPTIONS: '--openssl-legacy-provider',
153+
};
145154
}
146155

147-
await execp(bundleCommand, { cwd: workingDir });
156+
await execp(bundleCommand, execOptions);
148157
if (!(await fs.pathExists(bundleOutput))) {
149158
// Under some circumstances, Metro bundler process might fail
150159
// with some logs, but exit the process with a non error status code.
@@ -209,17 +218,14 @@ ${resetCache ? '--reset-cache' : ''}`;
209218
stdio: 'inherit',
210219
});
211220
} else {
212-
spawn(
213-
path.join(
214-
cwd,
215-
`node_modules/.bin/rnc-cli${os.platform() === 'win32' ? '.cmd' : ''}`,
216-
),
217-
['start', ...args],
218-
{
219-
cwd,
220-
stdio: 'inherit',
221-
},
222-
);
221+
// RN < 0.77: Use the project-local react-native CLI from node_modules
222+
// The project's react-native dependency ships its own CLI version
223+
// that includes the start command (via cli-plugin-metro)
224+
const localCliBin = path.join(cwd, 'node_modules', '.bin', 'react-native');
225+
spawn(localCliBin, ['start', ...args], {
226+
cwd,
227+
stdio: 'inherit',
228+
});
223229
}
224230
}
225231

@@ -329,22 +335,32 @@ ${resetCache ? '--reset-cache' : ''}`;
329335
const tmpDir = createTmpDir();
330336
const tmpScriptPath = path.join(tmpDir, scriptFileName);
331337

332-
// Check if we should use @react-native-community/cli for RN 0.77+
333-
let command = `${this.binaryPath} start ${args.join(' ')}`;
338+
// Determine which CLI to use based on RN version
339+
let command: string;
340+
let useCommunityCliLatest = false;
334341
try {
335342
const packageJsonPath = path.join(cwd, 'package.json');
336343
if (await fs.pathExists(packageJsonPath)) {
337344
const packageJson = await fs.readJSON(packageJsonPath);
338345
const rnVersion =
339346
packageJson.dependencies?.['react-native'] || '0.60.0';
340-
if (semver.gte(rnVersion.replace(/[\^~]/, ''), '0.77.0')) {
341-
command = `npx @react-native-community/cli start ${args.join(' ')}`;
342-
}
347+
useCommunityCliLatest = semver.gte(
348+
rnVersion.replace(/[\^~]/, ''),
349+
'0.77.0',
350+
);
343351
}
344352
} catch (e) {
345353
// If we can't determine version, use legacy approach
346354
}
347355

356+
if (useCommunityCliLatest) {
357+
command = `npx @react-native-community/cli start ${args.join(' ')}`;
358+
} else {
359+
// RN < 0.77: Use the project-local react-native CLI from node_modules
360+
const localCliBin = path.join(cwd, 'node_modules', '.bin', 'react-native');
361+
command = `${localCliBin} start ${args.join(' ')}`;
362+
}
363+
348364
await fs.writeFile(
349365
tmpScriptPath,
350366
`

0 commit comments

Comments
 (0)