Skip to content

Commit c64bb05

Browse files
feat: support ESM in react-native.config (#2453)
* feat: support esm in `react-native.config.js` * tests: add checks to validate is config is properly received * fix: units tests * fix: run each test in fresh environment * test: add proper test scenarios * Update __e2e__/config.test.ts Co-authored-by: Michał Pierzchała <[email protected]> * Update __e2e__/config.test.ts Co-authored-by: Michał Pierzchała <[email protected]> * refactor: create `loadConfigAsync` function --------- Co-authored-by: Michał Pierzchała <[email protected]>
1 parent 30b94f8 commit c64bb05

File tree

9 files changed

+349
-42
lines changed

9 files changed

+349
-42
lines changed

__e2e__/config.test.ts

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@ function createCorruptedSetupEnvScript() {
3535
};
3636
}
3737

38-
beforeAll(() => {
38+
const modifyPackageJson = (dir: string, key: string, value: string) => {
39+
const packageJsonPath = path.join(dir, 'package.json');
40+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
41+
packageJson[key] = value;
42+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
43+
};
44+
45+
beforeEach(() => {
3946
// Clean up folder and re-create a new project
4047
cleanup(DIR);
4148
writeFiles(DIR, {});
@@ -122,6 +129,34 @@ module.exports = {
122129
};
123130
`;
124131

132+
const USER_CONFIG_TS = `
133+
export default {
134+
commands: [
135+
{
136+
name: 'test-command-ts',
137+
description: 'test command',
138+
func: () => {
139+
console.log('test-command-ts');
140+
},
141+
},
142+
],
143+
};
144+
`;
145+
146+
const USER_CONFIG_ESM = `
147+
export default {
148+
commands: [
149+
{
150+
name: 'test-command-esm',
151+
description: 'test command',
152+
func: () => {
153+
console.log('test-command-esm');
154+
},
155+
},
156+
],
157+
};
158+
`;
159+
125160
test('should read user config from react-native.config.js', () => {
126161
writeFiles(path.join(DIR, 'TestProject'), {
127162
'react-native.config.js': USER_CONFIG,
@@ -133,9 +168,110 @@ test('should read user config from react-native.config.js', () => {
133168

134169
test('should read user config from react-native.config.ts', () => {
135170
writeFiles(path.join(DIR, 'TestProject'), {
136-
'react-native.config.ts': USER_CONFIG,
171+
'react-native.config.ts': USER_CONFIG_TS,
172+
});
173+
174+
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-ts']);
175+
expect(stdout).toBe('test-command-ts');
176+
});
177+
178+
test('should read user config from react-native.config.mjs', () => {
179+
writeFiles(path.join(DIR, 'TestProject'), {
180+
'react-native.config.mjs': USER_CONFIG_ESM,
181+
});
182+
183+
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
184+
expect(stdout).toBe('test-command-esm');
185+
});
186+
187+
test('should fail if using require() in ES module in react-native.config.mjs', () => {
188+
writeFiles(path.join(DIR, 'TestProject'), {
189+
'react-native.config.mjs': `
190+
const packageJSON = require('./package.json');
191+
${USER_CONFIG_ESM}
192+
`,
193+
});
194+
195+
const {stderr, stdout} = runCLI(path.join(DIR, 'TestProject'), [
196+
'test-command-esm',
197+
]);
198+
expect(stderr).toMatch('error Failed to load configuration of your project');
199+
expect(stdout).toMatch(
200+
'ReferenceError: require is not defined in ES module scope, you can use import instead',
201+
);
202+
});
203+
204+
test('should fail if using require() in ES module with "type": "module" in package.json', () => {
205+
writeFiles(path.join(DIR, 'TestProject'), {
206+
'react-native.config.js': `
207+
const packageJSON = require('./package.json');
208+
${USER_CONFIG_ESM}
209+
`,
137210
});
138211

212+
modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');
213+
214+
const {stderr} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
215+
console.log(stderr);
216+
expect(stderr).toMatch('error Failed to load configuration of your project');
217+
});
218+
219+
test('should read config if using createRequire() helper in react-native.config.js with "type": "module" in package.json', () => {
220+
writeFiles(path.join(DIR, 'TestProject'), {
221+
'react-native.config.js': `
222+
import { createRequire } from 'node:module';
223+
const require = createRequire(import.meta.url);
224+
const packageJSON = require('./package.json');
225+
226+
${USER_CONFIG_ESM}
227+
`,
228+
});
229+
230+
modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');
231+
232+
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
233+
expect(stdout).toBe('test-command-esm');
234+
});
235+
236+
test('should read config if using require() in react-native.config.cjs with "type": "module" in package.json', () => {
237+
writeFiles(path.join(DIR, 'TestProject'), {
238+
'react-native.config.cjs': `
239+
const packageJSON = require('./package.json');
240+
${USER_CONFIG}
241+
`,
242+
});
243+
244+
modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');
245+
139246
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command']);
140-
expect(stdout).toBe('test-command');
247+
expect(stdout).toMatch('test-command');
248+
});
249+
250+
test('should read config if using import/export in react-native.config.js with "type": "module" package.json', () => {
251+
writeFiles(path.join(DIR, 'TestProject'), {
252+
'react-native.config.js': `
253+
import {} from 'react';
254+
${USER_CONFIG_ESM}
255+
`,
256+
});
257+
258+
modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');
259+
260+
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
261+
expect(stdout).toMatch('test-command-esm');
262+
});
263+
264+
test('should read config if using import/export in react-native.config.mjs with "type": "commonjs" package.json', () => {
265+
writeFiles(path.join(DIR, 'TestProject'), {
266+
'react-native.config.mjs': `
267+
import {} from 'react';
268+
269+
${USER_CONFIG_ESM}
270+
`,
271+
});
272+
273+
modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'commonjs');
274+
275+
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
276+
expect(stdout).toMatch('test-command-esm');
141277
});

packages/cli-config/src/__tests__/index-test.ts

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'path';
22
import slash from 'slash';
3-
import loadConfig from '..';
3+
import {loadConfigAsync} from '..';
44
import {cleanup, writeFiles, getTempDirectory} from '../../../../jest/helpers';
55

66
let DIR = getTempDirectory('config_test');
@@ -59,18 +59,18 @@ beforeEach(async () => {
5959

6060
afterEach(() => cleanup(DIR));
6161

62-
test('should have a valid structure by default', () => {
62+
test('should have a valid structure by default', async () => {
6363
DIR = getTempDirectory('config_test_structure');
6464
writeFiles(DIR, {
6565
'react-native.config.js': `module.exports = {
6666
reactNativePath: "."
6767
}`,
6868
});
69-
const config = loadConfig({projectRoot: DIR});
69+
const config = await loadConfigAsync({projectRoot: DIR});
7070
expect(removeString(config, DIR)).toMatchSnapshot();
7171
});
7272

73-
test('should return dependencies from package.json', () => {
73+
test('should return dependencies from package.json', async () => {
7474
DIR = getTempDirectory('config_test_deps');
7575
writeFiles(DIR, {
7676
...REACT_NATIVE_MOCK,
@@ -83,11 +83,11 @@ test('should return dependencies from package.json', () => {
8383
}
8484
}`,
8585
});
86-
const {dependencies} = loadConfig({projectRoot: DIR});
86+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
8787
expect(removeString(dependencies, DIR)).toMatchSnapshot();
8888
});
8989

90-
test('should read a config of a dependency and use it to load other settings', () => {
90+
test('should read a config of a dependency and use it to load other settings', async () => {
9191
DIR = getTempDirectory('config_test_settings');
9292
writeFiles(DIR, {
9393
...REACT_NATIVE_MOCK,
@@ -122,13 +122,13 @@ test('should read a config of a dependency and use it to load other settings', (
122122
}
123123
}`,
124124
});
125-
const {dependencies} = loadConfig({projectRoot: DIR});
125+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
126126
expect(
127127
removeString(dependencies['react-native-test'], DIR),
128128
).toMatchSnapshot();
129129
});
130130

131-
test('command specified in root config should overwrite command in "react-native-foo" and "react-native-bar" packages', () => {
131+
test('command specified in root config should overwrite command in "react-native-foo" and "react-native-bar" packages', async () => {
132132
DIR = getTempDirectory('config_test_packages');
133133
writeFiles(DIR, {
134134
'node_modules/react-native-foo/package.json': '{}',
@@ -173,15 +173,15 @@ test('command specified in root config should overwrite command in "react-native
173173
],
174174
};`,
175175
});
176-
const {commands} = loadConfig({projectRoot: DIR});
176+
const {commands} = await loadConfigAsync({projectRoot: DIR});
177177
const commandsNames = commands.map(({name}) => name);
178178
const commandIndex = commandsNames.indexOf('foo-command');
179179

180180
expect(commands[commandIndex].options).not.toBeNull();
181181
expect(commands[commandIndex]).toMatchSnapshot();
182182
});
183183

184-
test('should merge project configuration with default values', () => {
184+
test('should merge project configuration with default values', async () => {
185185
DIR = getTempDirectory('config_test_merge');
186186
writeFiles(DIR, {
187187
...REACT_NATIVE_MOCK,
@@ -206,13 +206,13 @@ test('should merge project configuration with default values', () => {
206206
}
207207
}`,
208208
});
209-
const {dependencies} = loadConfig({projectRoot: DIR});
209+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
210210
expect(removeString(dependencies['react-native-test'], DIR)).toMatchSnapshot(
211211
'snapshoting `react-native-test` config',
212212
);
213213
});
214214

215-
test('should load commands from "react-native-foo" and "react-native-bar" packages', () => {
215+
test('should load commands from "react-native-foo" and "react-native-bar" packages', async () => {
216216
DIR = getTempDirectory('config_test_packages');
217217
writeFiles(DIR, {
218218
'react-native.config.js': 'module.exports = { reactNativePath: "." }',
@@ -241,11 +241,11 @@ test('should load commands from "react-native-foo" and "react-native-bar" packag
241241
}
242242
}`,
243243
});
244-
const {commands} = loadConfig({projectRoot: DIR});
244+
const {commands} = await loadConfigAsync({projectRoot: DIR});
245245
expect(commands).toMatchSnapshot();
246246
});
247247

248-
test('should not skip packages that have invalid configuration (to avoid breaking users)', () => {
248+
test('should not skip packages that have invalid configuration (to avoid breaking users)', async () => {
249249
process.env.FORCE_COLOR = '0'; // To disable chalk
250250
DIR = getTempDirectory('config_test_skip');
251251
writeFiles(DIR, {
@@ -261,14 +261,14 @@ test('should not skip packages that have invalid configuration (to avoid breakin
261261
}
262262
}`,
263263
});
264-
const {dependencies} = loadConfig({projectRoot: DIR});
264+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
265265
expect(removeString(dependencies, DIR)).toMatchSnapshot(
266266
'dependencies config',
267267
);
268268
expect(spy.mock.calls[0][0]).toMatchSnapshot('logged warning');
269269
});
270270

271-
test('does not use restricted "react-native" key to resolve config from package.json', () => {
271+
test('does not use restricted "react-native" key to resolve config from package.json', async () => {
272272
DIR = getTempDirectory('config_test_restricted');
273273
writeFiles(DIR, {
274274
'node_modules/react-native-netinfo/package.json': `{
@@ -281,12 +281,12 @@ test('does not use restricted "react-native" key to resolve config from package.
281281
}
282282
}`,
283283
});
284-
const {dependencies} = loadConfig({projectRoot: DIR});
284+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
285285
expect(dependencies).toHaveProperty('react-native-netinfo');
286286
expect(spy).not.toHaveBeenCalled();
287287
});
288288

289-
test('supports dependencies from user configuration with custom root and properties', () => {
289+
test('supports dependencies from user configuration with custom root and properties', async () => {
290290
DIR = getTempDirectory('config_test_custom_root');
291291
const escapePathSeparator = (value: string) =>
292292
path.sep === '\\' ? value.replace(/(\/|\\)/g, '\\\\') : value;
@@ -327,7 +327,7 @@ module.exports = {
327327
}`,
328328
});
329329

330-
const {dependencies} = loadConfig({projectRoot: DIR});
330+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
331331
expect(removeString(dependencies['local-lib'], DIR)).toMatchInlineSnapshot(`
332332
Object {
333333
"name": "local-lib",
@@ -345,7 +345,7 @@ module.exports = {
345345
`);
346346
});
347347

348-
test('should apply build types from dependency config', () => {
348+
test('should apply build types from dependency config', async () => {
349349
DIR = getTempDirectory('config_test_apply_dependency_config');
350350
writeFiles(DIR, {
351351
...REACT_NATIVE_MOCK,
@@ -367,13 +367,13 @@ test('should apply build types from dependency config', () => {
367367
}
368368
}`,
369369
});
370-
const {dependencies} = loadConfig({projectRoot: DIR});
370+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
371371
expect(
372372
removeString(dependencies['react-native-test'], DIR),
373373
).toMatchSnapshot();
374374
});
375375

376-
test('supports dependencies from user configuration with custom build type', () => {
376+
test('supports dependencies from user configuration with custom build type', async () => {
377377
DIR = getTempDirectory('config_test_apply_custom_build_config');
378378
writeFiles(DIR, {
379379
...REACT_NATIVE_MOCK,
@@ -400,13 +400,13 @@ test('supports dependencies from user configuration with custom build type', ()
400400
}`,
401401
});
402402

403-
const {dependencies} = loadConfig({projectRoot: DIR});
403+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
404404
expect(
405405
removeString(dependencies['react-native-test'], DIR),
406406
).toMatchSnapshot();
407407
});
408408

409-
test('supports disabling dependency for ios platform', () => {
409+
test('supports disabling dependency for ios platform', async () => {
410410
DIR = getTempDirectory('config_test_disable_dependency_platform');
411411
writeFiles(DIR, {
412412
...REACT_NATIVE_MOCK,
@@ -429,13 +429,13 @@ test('supports disabling dependency for ios platform', () => {
429429
}`,
430430
});
431431

432-
const {dependencies} = loadConfig({projectRoot: DIR});
432+
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
433433
expect(
434434
removeString(dependencies['react-native-test'], DIR),
435435
).toMatchSnapshot();
436436
});
437437

438-
test('should convert project sourceDir relative path to absolute', () => {
438+
test('should convert project sourceDir relative path to absolute', async () => {
439439
DIR = getTempDirectory('config_test_absolute_project_source_dir');
440440
const iosProjectDir = './ios2';
441441
const androidProjectDir = './android2';
@@ -494,7 +494,7 @@ test('should convert project sourceDir relative path to absolute', () => {
494494
`,
495495
});
496496

497-
const config = loadConfig({projectRoot: DIR});
497+
const config = await loadConfigAsync({projectRoot: DIR});
498498

499499
expect(config.project.ios?.sourceDir).toBe(path.join(DIR, iosProjectDir));
500500
expect(config.project.android?.sourceDir).toBe(

packages/cli-config/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import config from './commands/config';
22

33
export {default} from './loadConfig';
4+
export {loadConfigAsync} from './loadConfig';
45

56
export const commands = [config];

0 commit comments

Comments
 (0)