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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
23 changes: 23 additions & 0 deletions __e2e__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ module.exports = {
};
`;

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 @@ -139,3 +153,12 @@ test('should read user config from react-native.config.ts', () => {
const {stdout} = runCLI(path.join(DIR, 'TestProject'), ['test-command']);
expect(stdout).toBe('test-command');
});

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');
});
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
15 changes: 8 additions & 7 deletions packages/cli-config/src/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ const removeDuplicateCommands = <T extends boolean>(commands: Command<T>[]) => {
/**
* Loads CLI configuration
*/
function loadConfig({
async function loadConfig({
projectRoot = findProjectRoot(),
selectedPlatform,
}: {
projectRoot?: string;
selectedPlatform?: string;
}): Config {
}): Promise<Config> {
let lazyProject: ProjectConfig;
const userConfig = readConfigFromDisk(projectRoot);
const userConfig = await readConfigFromDisk(projectRoot);

const initialConfig: Config = {
root: projectRoot,
Expand Down Expand Up @@ -130,20 +130,21 @@ function loadConfig({
},
};

const finalConfig = Array.from(
const finalConfig = await Array.from(
new Set([
...Object.keys(userConfig.dependencies),
...findDependencies(projectRoot),
]),
).reduce((acc: Config, dependencyName) => {
).reduce(async (accPromise: Promise<Config>, dependencyName) => {
const acc = await accPromise;
const localDependencyRoot =
userConfig.dependencies[dependencyName] &&
userConfig.dependencies[dependencyName].root;
try {
let root =
localDependencyRoot ||
resolveNodeModuleDir(projectRoot, dependencyName);
let config = readDependencyConfigFromDisk(root, dependencyName);
let config = await readDependencyConfigFromDisk(root, dependencyName);

return assign({}, acc, {
dependencies: assign({}, acc.dependencies, {
Expand Down Expand Up @@ -172,7 +173,7 @@ function loadConfig({
} catch {
return acc;
}
}, initialConfig);
}, Promise.resolve(initialConfig));

return finalConfig;
}
Expand Down
25 changes: 16 additions & 9 deletions packages/cli-config/src/readConfigFromDisk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {cosmiconfigSync} from 'cosmiconfig';
import {cosmiconfig} from 'cosmiconfig';
import {JoiError} from './errors';
import * as schema from './schema';
import {
Expand All @@ -11,19 +11,26 @@ import chalk from 'chalk';
/**
* Places to look for the configuration file.
*/
const searchPlaces = ['react-native.config.js', 'react-native.config.ts'];
const searchPlaces = [
'react-native.config.js',
'react-native.config.ts',
'react-native.config.mjs',
];

/**
* Reads a project configuration as defined by the user in the current
* workspace.
*/
export function readConfigFromDisk(rootFolder: string): UserConfig {
const explorer = cosmiconfigSync('react-native', {
export async function readConfigFromDisk(
rootFolder: string,
): Promise<UserConfig> {
const explorer = cosmiconfig('react-native', {
stopDir: rootFolder,
searchPlaces,
});

const searchResult = explorer.search(rootFolder);
const searchResult = await explorer.search(rootFolder);

const config = searchResult ? searchResult.config : undefined;
const result = schema.projectConfig.validate(config);

Expand All @@ -38,16 +45,16 @@ export function readConfigFromDisk(rootFolder: string): UserConfig {
* Reads a dependency configuration as defined by the developer
* inside `node_modules`.
*/
export function readDependencyConfigFromDisk(
export async function readDependencyConfigFromDisk(
rootFolder: string,
dependencyName: string,
): UserDependencyConfig {
const explorer = cosmiconfigSync('react-native', {
): Promise<UserDependencyConfig> {
const explorer = cosmiconfig('react-native', {
stopDir: rootFolder,
searchPlaces,
});

const searchResult = explorer.search(rootFolder);
const searchResult = await explorer.search(rootFolder);
const config = searchResult ? searchResult.config : emptyDependencyConfig;

const result = schema.dependencyConfig.validate(config, {abortEarly: false});
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-doctor/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const doctorCommand = (async (_, options, config) => {
Promise.all(categories.map(iterateOverHealthChecks));

const healthchecksPerCategory = await iterateOverCategories(
Object.values(getHealthchecks(options)).filter(
Object.values(await getHealthchecks(options)).filter(
(category) => category !== undefined,
) as HealthCheckCategory[],
);
Expand Down
6 changes: 4 additions & 2 deletions packages/cli-doctor/src/tools/healthchecks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ type Options = {
contributor: boolean | void;
};

export const getHealthchecks = ({contributor}: Options): Healthchecks => {
export const getHealthchecks = async ({
contributor,
}: Options): Promise<Healthchecks> => {
let additionalChecks: HealthCheckCategory[] = [];
let projectSpecificHealthchecks = {};
let config;

// Doctor can run in a detached mode, where there isn't a config so this can fail
try {
config = loadConfig({});
config = await loadConfig({});
additionalChecks = config.healthChecks;

if (config.reactNativePath) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ async function setupAndRun(platformName?: string) {
}
}

config = loadConfig({
config = await loadConfig({
selectedPlatform,
});

Expand Down
Loading