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 5 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 if using require() in ES module in react-native.config.mjs', () => {
szymonrybczak marked this conversation as resolved.
Show resolved Hide resolved
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 if using require() in ES module with "type": "module" in package.json', () => {
szymonrybczak marked this conversation as resolved.
Show resolved Hide resolved
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');
});
52 changes: 26 additions & 26 deletions packages/cli-config/src/__tests__/index-test.ts
Original file line number Diff line number Diff line change
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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({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 loadConfig({projectRoot: DIR});

expect(config.project.ios?.sourceDir).toBe(path.join(DIR, iosProjectDir));
expect(config.project.android?.sourceDir).toBe(
Expand Down
Loading
Loading