Skip to content

Commit 01834f0

Browse files
committed
fix(detox): expo stopped supporting detox in expo 54
1 parent 60583b8 commit 01834f0

File tree

8 files changed

+360
-1
lines changed

8 files changed

+360
-1
lines changed

packages/detox/migrations.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
{
2-
"generators": {},
2+
"generators": {
3+
"update-22-0-0-remove-config-plugins-detox-for-expo-54": {
4+
"requires": {
5+
"expo": ">= 54.0.0"
6+
},
7+
"version": "22.2.0-beta.2",
8+
"cli": "nx",
9+
"description": "Remove @config-plugins/detox for Expo 54+ projects (package discontinued)",
10+
"factory": "./src/migrations/update-22-0-0/remove-config-plugins-detox-for-expo-54"
11+
}
12+
},
313
"packageJsonUpdates": {
414
"20.3.0": {
515
"version": "20.3.0-beta.0",

packages/detox/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@nx/js": "workspace:*",
3333
"@nx/eslint": "workspace:*",
3434
"@nx/react": "workspace:*",
35+
"semver": "catalog:",
3536
"tslib": "catalog:typescript"
3637
},
3738
"devDependencies": {

packages/detox/src/generators/application/application.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,14 @@ describe('detox application generator', () => {
330330
root: 'my-dir/my-app',
331331
});
332332

333+
// Add Expo 53 to package.json to allow expo framework tests to run
334+
// Expo 54+ is not supported due to @config-plugins/detox being discontinued
335+
updateJson(tree, 'package.json', (json) => {
336+
json.dependencies = json.dependencies || {};
337+
json.dependencies['expo'] = '~53.0.0';
338+
return json;
339+
});
340+
333341
await detoxApplicationGenerator(tree, {
334342
e2eDirectory: 'my-dir/my-app-e2e',
335343
appProject: 'my-dir-my-app',
@@ -406,6 +414,55 @@ describe('detox application generator', () => {
406414
});
407415
});
408416

417+
describe('expo 54+ (unsupported)', () => {
418+
it('should throw error for Expo 54+ with framework expo', async () => {
419+
addProjectConfiguration(tree, 'my-app', {
420+
root: 'my-app',
421+
});
422+
423+
// Add Expo 54 to package.json
424+
updateJson(tree, 'package.json', (json) => {
425+
json.dependencies = json.dependencies || {};
426+
json.dependencies['expo'] = '~54.0.0';
427+
return json;
428+
});
429+
430+
await expect(
431+
detoxApplicationGenerator(tree, {
432+
e2eDirectory: 'my-app-e2e',
433+
appProject: 'my-app',
434+
linter: 'none',
435+
framework: 'expo',
436+
addPlugin: true,
437+
})
438+
).rejects.toThrow(/Detox with Expo 54\+ is not supported/);
439+
});
440+
441+
it('should allow react-native framework with Expo 54+', async () => {
442+
addProjectConfiguration(tree, 'my-app', {
443+
root: 'my-app',
444+
});
445+
446+
// Add Expo 54 to package.json
447+
updateJson(tree, 'package.json', (json) => {
448+
json.dependencies = json.dependencies || {};
449+
json.dependencies['expo'] = '~54.0.0';
450+
return json;
451+
});
452+
453+
// Should not throw - react-native framework works with any Expo version
454+
await detoxApplicationGenerator(tree, {
455+
e2eDirectory: 'my-app-e2e',
456+
appProject: 'my-app',
457+
linter: 'none',
458+
framework: 'react-native',
459+
addPlugin: true,
460+
});
461+
462+
expect(tree.exists('my-app-e2e/.detoxrc.json')).toBeTruthy();
463+
});
464+
});
465+
409466
describe('tsconfig', () => {
410467
beforeEach(async () => {
411468
addProjectConfiguration(tree, 'my-app', { root: 'my-app' });

packages/detox/src/generators/application/application.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
updateTsconfigFiles,
1616
} from '@nx/js/src/utils/typescript/ts-solution-setup';
1717
import { sortPackageJsonFields } from '@nx/js/src/utils/package-json/sort-fields';
18+
import { isExpoV54OrAbove } from '../../utils/expo-version-utils';
1819

1920
export async function detoxApplicationGenerator(host: Tree, schema: Schema) {
2021
return await detoxApplicationGeneratorInternal(host, {
@@ -36,6 +37,19 @@ export async function detoxApplicationGeneratorInternal(
3637

3738
const options = await normalizeOptions(host, schema);
3839

40+
// Validate Expo version compatibility
41+
// @config-plugins/detox was discontinued and is incompatible with Expo 54+
42+
// See: https://github.com/expo/config-plugins/pull/290
43+
if (options.framework === 'expo' && isExpoV54OrAbove(host)) {
44+
throw new Error(
45+
`Detox with Expo 54+ is not supported. The @config-plugins/detox package has been discontinued ` +
46+
`and is incompatible with Expo 54. Please consider one of the following alternatives:\n` +
47+
` - Use framework: 'react-native' instead of 'expo'\n` +
48+
` - Use Maestro for E2E testing (recommended by Expo): https://docs.expo.dev/build-reference/e2e-tests/\n` +
49+
` - Stay on Expo 53 if you need Detox support`
50+
);
51+
}
52+
3953
const initTask = await detoxInitGenerator(host, {
4054
...options,
4155
skipFormat: true,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { Tree, readJson, writeJson } from '@nx/devkit';
2+
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
3+
import update from './remove-config-plugins-detox-for-expo-54';
4+
5+
describe('remove-config-plugins-detox-for-expo-54 migration', () => {
6+
let tree: Tree;
7+
8+
beforeEach(() => {
9+
tree = createTreeWithEmptyWorkspace();
10+
});
11+
12+
it('should remove @config-plugins/detox for Expo 54+ projects', async () => {
13+
writeJson(tree, 'package.json', {
14+
name: '@proj/source',
15+
dependencies: {
16+
expo: '~54.0.0',
17+
},
18+
devDependencies: {
19+
'@config-plugins/detox': '~11.0.0',
20+
},
21+
});
22+
23+
await update(tree);
24+
25+
const packageJson = readJson(tree, 'package.json');
26+
expect(
27+
packageJson.devDependencies['@config-plugins/detox']
28+
).toBeUndefined();
29+
});
30+
31+
it('should remove @config-plugins/detox from dependencies if present there', async () => {
32+
writeJson(tree, 'package.json', {
33+
name: '@proj/source',
34+
dependencies: {
35+
expo: '~54.0.0',
36+
'@config-plugins/detox': '~11.0.0',
37+
},
38+
});
39+
40+
await update(tree);
41+
42+
const packageJson = readJson(tree, 'package.json');
43+
expect(packageJson.dependencies['@config-plugins/detox']).toBeUndefined();
44+
});
45+
46+
it('should NOT remove @config-plugins/detox for Expo 53 projects', async () => {
47+
writeJson(tree, 'package.json', {
48+
name: '@proj/source',
49+
dependencies: {
50+
expo: '~53.0.0',
51+
},
52+
devDependencies: {
53+
'@config-plugins/detox': '~11.0.0',
54+
},
55+
});
56+
57+
await update(tree);
58+
59+
const packageJson = readJson(tree, 'package.json');
60+
expect(packageJson.devDependencies['@config-plugins/detox']).toBe(
61+
'~11.0.0'
62+
);
63+
});
64+
65+
it('should skip if @config-plugins/detox is not installed', async () => {
66+
writeJson(tree, 'package.json', {
67+
name: '@proj/source',
68+
dependencies: {
69+
expo: '~54.0.0',
70+
},
71+
devDependencies: {
72+
detox: '~20.43.0',
73+
},
74+
});
75+
76+
await update(tree);
77+
78+
const packageJson = readJson(tree, 'package.json');
79+
expect(packageJson.devDependencies['detox']).toBe('~20.43.0');
80+
});
81+
82+
it('should skip if expo is not installed (React Native project)', async () => {
83+
writeJson(tree, 'package.json', {
84+
name: '@proj/source',
85+
dependencies: {
86+
'react-native': '~0.81.0',
87+
},
88+
devDependencies: {
89+
'@config-plugins/detox': '~11.0.0',
90+
},
91+
});
92+
93+
await update(tree);
94+
95+
const packageJson = readJson(tree, 'package.json');
96+
// Should keep @config-plugins/detox for React Native projects
97+
expect(packageJson.devDependencies['@config-plugins/detox']).toBe(
98+
'~11.0.0'
99+
);
100+
});
101+
102+
it('should handle caret version ranges for Expo 54+', async () => {
103+
writeJson(tree, 'package.json', {
104+
name: '@proj/source',
105+
dependencies: {
106+
expo: '^54.0.0',
107+
},
108+
devDependencies: {
109+
'@config-plugins/detox': '~11.0.0',
110+
},
111+
});
112+
113+
await update(tree);
114+
115+
const packageJson = readJson(tree, 'package.json');
116+
expect(
117+
packageJson.devDependencies['@config-plugins/detox']
118+
).toBeUndefined();
119+
});
120+
121+
it('should handle exact version for Expo 54+', async () => {
122+
writeJson(tree, 'package.json', {
123+
name: '@proj/source',
124+
dependencies: {
125+
expo: '54.0.25',
126+
},
127+
devDependencies: {
128+
'@config-plugins/detox': '~11.0.0',
129+
},
130+
});
131+
132+
await update(tree);
133+
134+
const packageJson = readJson(tree, 'package.json');
135+
expect(
136+
packageJson.devDependencies['@config-plugins/detox']
137+
).toBeUndefined();
138+
});
139+
140+
it('should handle missing package.json gracefully', async () => {
141+
tree.delete('package.json');
142+
143+
await expect(update(tree)).resolves.not.toThrow();
144+
});
145+
146+
it('should handle unparseable expo version gracefully', async () => {
147+
writeJson(tree, 'package.json', {
148+
name: '@proj/source',
149+
dependencies: {
150+
expo: 'invalid-version',
151+
},
152+
devDependencies: {
153+
'@config-plugins/detox': '~11.0.0',
154+
},
155+
});
156+
157+
await update(tree);
158+
159+
const packageJson = readJson(tree, 'package.json');
160+
// Should keep @config-plugins/detox when version can't be parsed
161+
expect(packageJson.devDependencies['@config-plugins/detox']).toBe(
162+
'~11.0.0'
163+
);
164+
});
165+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Tree, updateJson, logger, readJson, formatFiles } from '@nx/devkit';
2+
import { clean, coerce, major } from 'semver';
3+
4+
/**
5+
* Remove @config-plugins/detox for Expo 54+ projects.
6+
*
7+
* The @config-plugins/detox package has been discontinued and is incompatible
8+
* with Expo 54. See: https://github.com/expo/config-plugins/pull/290
9+
*
10+
* This migration removes the dependency from package.json for projects
11+
* using Expo 54 or above.
12+
*/
13+
export default async function update(tree: Tree) {
14+
const rootPackageJsonPath = 'package.json';
15+
16+
if (!tree.exists(rootPackageJsonPath)) {
17+
return;
18+
}
19+
20+
const packageJson = readJson(tree, rootPackageJsonPath);
21+
22+
// Check if @config-plugins/detox is installed
23+
const hasConfigPluginsDetox =
24+
packageJson.dependencies?.['@config-plugins/detox'] ||
25+
packageJson.devDependencies?.['@config-plugins/detox'];
26+
27+
if (!hasConfigPluginsDetox) {
28+
return;
29+
}
30+
31+
// Check the installed Expo version
32+
const expoVersion =
33+
packageJson.dependencies?.['expo'] || packageJson.devDependencies?.['expo'];
34+
35+
if (!expoVersion) {
36+
// No expo installed, this is likely a React Native project, skip
37+
return;
38+
}
39+
40+
const cleanedVersion = clean(expoVersion) ?? coerce(expoVersion)?.version;
41+
if (!cleanedVersion) {
42+
logger.warn(
43+
`Could not parse Expo version "${expoVersion}". Skipping @config-plugins/detox removal.`
44+
);
45+
return;
46+
}
47+
48+
const majorVersion = major(cleanedVersion);
49+
50+
if (majorVersion < 54) {
51+
// Expo 53 or below still supports @config-plugins/detox
52+
return;
53+
}
54+
55+
// Remove @config-plugins/detox from package.json
56+
updateJson(tree, rootPackageJsonPath, (json) => {
57+
if (json.dependencies?.['@config-plugins/detox']) {
58+
delete json.dependencies['@config-plugins/detox'];
59+
}
60+
if (json.devDependencies?.['@config-plugins/detox']) {
61+
delete json.devDependencies['@config-plugins/detox'];
62+
}
63+
return json;
64+
});
65+
66+
logger.warn(
67+
`Removed @config-plugins/detox from package.json.\n` +
68+
`The @config-plugins/detox package has been discontinued and is incompatible with Expo 54+.\n` +
69+
`For E2E testing with Expo 54+, consider using Maestro: https://docs.expo.dev/build-reference/e2e-tests/`
70+
);
71+
72+
await formatFiles(tree);
73+
}

0 commit comments

Comments
 (0)