Skip to content

Commit

Permalink
feat: support ESM in react-native.config (#2453)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
szymonrybczak and thymikee authored Nov 5, 2024
1 parent 30b94f8 commit c64bb05
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 42 deletions.
142 changes: 139 additions & 3 deletions __e2e__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ function createCorruptedSetupEnvScript() {
};
}

beforeAll(() => {
const modifyPackageJson = (dir: string, key: string, value: string) => {
const packageJsonPath = path.join(dir, 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson[key] = value;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
};

beforeEach(() => {
// Clean up folder and re-create a new project
cleanup(DIR);
writeFiles(DIR, {});
Expand Down Expand Up @@ -122,6 +129,34 @@ module.exports = {
};
`;

const USER_CONFIG_TS = `
export default {
commands: [
{
name: 'test-command-ts',
description: 'test command',
func: () => {
console.log('test-command-ts');
},
},
],
};
`;

const USER_CONFIG_ESM = `
export default {
commands: [
{
name: 'test-command-esm',
description: 'test command',
func: () => {
console.log('test-command-esm');
},
},
],
};
`;

test('should read user config from react-native.config.js', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.js': USER_CONFIG,
Expand All @@ -133,9 +168,110 @@ test('should read user config from react-native.config.js', () => {

test('should read user config from react-native.config.ts', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.ts': USER_CONFIG,
'react-native.config.ts': USER_CONFIG_TS,
});

const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-ts']);
expect(stdout).toBe('test-command-ts');
});

test('should read user config from react-native.config.mjs', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.mjs': USER_CONFIG_ESM,
});

const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
expect(stdout).toBe('test-command-esm');
});

test('should fail if using require() in ES module in react-native.config.mjs', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.mjs': `
const packageJSON = require('./package.json');
${USER_CONFIG_ESM}
`,
});

const {stderr, stdout} = runCLI(path.join(DIR, 'TestProject'), [
'test-command-esm',
]);
expect(stderr).toMatch('error Failed to load configuration of your project');
expect(stdout).toMatch(
'ReferenceError: require is not defined in ES module scope, you can use import instead',
);
});

test('should fail if using require() in ES module with "type": "module" in package.json', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.js': `
const packageJSON = require('./package.json');
${USER_CONFIG_ESM}
`,
});

modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');

const {stderr} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
console.log(stderr);
expect(stderr).toMatch('error Failed to load configuration of your project');
});

test('should read config if using createRequire() helper in react-native.config.js with "type": "module" in package.json', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.js': `
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const packageJSON = require('./package.json');
${USER_CONFIG_ESM}
`,
});

modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');

const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
expect(stdout).toBe('test-command-esm');
});

test('should read config if using require() in react-native.config.cjs with "type": "module" in package.json', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.cjs': `
const packageJSON = require('./package.json');
${USER_CONFIG}
`,
});

modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');

const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command']);
expect(stdout).toBe('test-command');
expect(stdout).toMatch('test-command');
});

test('should read config if using import/export in react-native.config.js with "type": "module" package.json', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.js': `
import {} from 'react';
${USER_CONFIG_ESM}
`,
});

modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'module');

const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
expect(stdout).toMatch('test-command-esm');
});

test('should read config if using import/export in react-native.config.mjs with "type": "commonjs" package.json', () => {
writeFiles(path.join(DIR, 'TestProject'), {
'react-native.config.mjs': `
import {} from 'react';
${USER_CONFIG_ESM}
`,
});

modifyPackageJson(path.join(DIR, 'TestProject'), 'type', 'commonjs');

const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command-esm']);
expect(stdout).toMatch('test-command-esm');
});
54 changes: 27 additions & 27 deletions packages/cli-config/src/__tests__/index-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from 'path';
import slash from 'slash';
import loadConfig from '..';
import {loadConfigAsync} from '..';
import {cleanup, writeFiles, getTempDirectory} from '../../../../jest/helpers';

let DIR = getTempDirectory('config_test');
Expand Down Expand Up @@ -59,18 +59,18 @@ beforeEach(async () => {

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

test('should have a valid structure by default', () => {
test('should have a valid structure by default', async () => {
DIR = getTempDirectory('config_test_structure');
writeFiles(DIR, {
'react-native.config.js': `module.exports = {
reactNativePath: "."
}`,
});
const config = loadConfig({projectRoot: DIR});
const config = await loadConfigAsync({projectRoot: DIR});
expect(removeString(config, DIR)).toMatchSnapshot();
});

test('should return dependencies from package.json', () => {
test('should return dependencies from package.json', async () => {
DIR = getTempDirectory('config_test_deps');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
Expand All @@ -83,11 +83,11 @@ test('should return dependencies from package.json', () => {
}
}`,
});
const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(removeString(dependencies, DIR)).toMatchSnapshot();
});

test('should read a config of a dependency and use it to load other settings', () => {
test('should read a config of a dependency and use it to load other settings', async () => {
DIR = getTempDirectory('config_test_settings');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
Expand Down Expand Up @@ -122,13 +122,13 @@ test('should read a config of a dependency and use it to load other settings', (
}
}`,
});
const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(
removeString(dependencies['react-native-test'], DIR),
).toMatchSnapshot();
});

test('command specified in root config should overwrite command in "react-native-foo" and "react-native-bar" packages', () => {
test('command specified in root config should overwrite command in "react-native-foo" and "react-native-bar" packages', async () => {
DIR = getTempDirectory('config_test_packages');
writeFiles(DIR, {
'node_modules/react-native-foo/package.json': '{}',
Expand Down Expand Up @@ -173,15 +173,15 @@ test('command specified in root config should overwrite command in "react-native
],
};`,
});
const {commands} = loadConfig({projectRoot: DIR});
const {commands} = await loadConfigAsync({projectRoot: DIR});
const commandsNames = commands.map(({name}) => name);
const commandIndex = commandsNames.indexOf('foo-command');

expect(commands[commandIndex].options).not.toBeNull();
expect(commands[commandIndex]).toMatchSnapshot();
});

test('should merge project configuration with default values', () => {
test('should merge project configuration with default values', async () => {
DIR = getTempDirectory('config_test_merge');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
Expand All @@ -206,13 +206,13 @@ test('should merge project configuration with default values', () => {
}
}`,
});
const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(removeString(dependencies['react-native-test'], DIR)).toMatchSnapshot(
'snapshoting `react-native-test` config',
);
});

test('should load commands from "react-native-foo" and "react-native-bar" packages', () => {
test('should load commands from "react-native-foo" and "react-native-bar" packages', async () => {
DIR = getTempDirectory('config_test_packages');
writeFiles(DIR, {
'react-native.config.js': 'module.exports = { reactNativePath: "." }',
Expand Down Expand Up @@ -241,11 +241,11 @@ test('should load commands from "react-native-foo" and "react-native-bar" packag
}
}`,
});
const {commands} = loadConfig({projectRoot: DIR});
const {commands} = await loadConfigAsync({projectRoot: DIR});
expect(commands).toMatchSnapshot();
});

test('should not skip packages that have invalid configuration (to avoid breaking users)', () => {
test('should not skip packages that have invalid configuration (to avoid breaking users)', async () => {
process.env.FORCE_COLOR = '0'; // To disable chalk
DIR = getTempDirectory('config_test_skip');
writeFiles(DIR, {
Expand All @@ -261,14 +261,14 @@ test('should not skip packages that have invalid configuration (to avoid breakin
}
}`,
});
const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(removeString(dependencies, DIR)).toMatchSnapshot(
'dependencies config',
);
expect(spy.mock.calls[0][0]).toMatchSnapshot('logged warning');
});

test('does not use restricted "react-native" key to resolve config from package.json', () => {
test('does not use restricted "react-native" key to resolve config from package.json', async () => {
DIR = getTempDirectory('config_test_restricted');
writeFiles(DIR, {
'node_modules/react-native-netinfo/package.json': `{
Expand All @@ -281,12 +281,12 @@ test('does not use restricted "react-native" key to resolve config from package.
}
}`,
});
const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(dependencies).toHaveProperty('react-native-netinfo');
expect(spy).not.toHaveBeenCalled();
});

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

const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(removeString(dependencies['local-lib'], DIR)).toMatchInlineSnapshot(`
Object {
"name": "local-lib",
Expand All @@ -345,7 +345,7 @@ module.exports = {
`);
});

test('should apply build types from dependency config', () => {
test('should apply build types from dependency config', async () => {
DIR = getTempDirectory('config_test_apply_dependency_config');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
Expand All @@ -367,13 +367,13 @@ test('should apply build types from dependency config', () => {
}
}`,
});
const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(
removeString(dependencies['react-native-test'], DIR),
).toMatchSnapshot();
});

test('supports dependencies from user configuration with custom build type', () => {
test('supports dependencies from user configuration with custom build type', async () => {
DIR = getTempDirectory('config_test_apply_custom_build_config');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
Expand All @@ -400,13 +400,13 @@ test('supports dependencies from user configuration with custom build type', ()
}`,
});

const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(
removeString(dependencies['react-native-test'], DIR),
).toMatchSnapshot();
});

test('supports disabling dependency for ios platform', () => {
test('supports disabling dependency for ios platform', async () => {
DIR = getTempDirectory('config_test_disable_dependency_platform');
writeFiles(DIR, {
...REACT_NATIVE_MOCK,
Expand All @@ -429,13 +429,13 @@ test('supports disabling dependency for ios platform', () => {
}`,
});

const {dependencies} = loadConfig({projectRoot: DIR});
const {dependencies} = await loadConfigAsync({projectRoot: DIR});
expect(
removeString(dependencies['react-native-test'], DIR),
).toMatchSnapshot();
});

test('should convert project sourceDir relative path to absolute', () => {
test('should convert project sourceDir relative path to absolute', async () => {
DIR = getTempDirectory('config_test_absolute_project_source_dir');
const iosProjectDir = './ios2';
const androidProjectDir = './android2';
Expand Down Expand Up @@ -494,7 +494,7 @@ test('should convert project sourceDir relative path to absolute', () => {
`,
});

const config = loadConfig({projectRoot: DIR});
const config = await loadConfigAsync({projectRoot: DIR});

expect(config.project.ios?.sourceDir).toBe(path.join(DIR, iosProjectDir));
expect(config.project.android?.sourceDir).toBe(
Expand Down
1 change: 1 addition & 0 deletions packages/cli-config/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import config from './commands/config';

export {default} from './loadConfig';
export {loadConfigAsync} from './loadConfig';

export const commands = [config];
Loading

0 comments on commit c64bb05

Please sign in to comment.