Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ESM in react-native.config #2453

Merged
merged 8 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading