From 77fd74b9263f1fe997784599beb40bce3d647b65 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 9 Jul 2024 16:30:53 +0200 Subject: [PATCH 1/8] feat: support esm in `react-native.config.js` --- packages/cli-config/src/loadConfig.ts | 15 +++++------ packages/cli-config/src/readConfigFromDisk.ts | 25 ++++++++++++------- packages/cli-doctor/src/commands/doctor.ts | 2 +- .../src/tools/healthchecks/index.ts | 6 +++-- packages/cli/src/index.ts | 2 +- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/cli-config/src/loadConfig.ts b/packages/cli-config/src/loadConfig.ts index d0a9ceac8..82b9a062c 100644 --- a/packages/cli-config/src/loadConfig.ts +++ b/packages/cli-config/src/loadConfig.ts @@ -85,15 +85,15 @@ const removeDuplicateCommands = (commands: Command[]) => { /** * Loads CLI configuration */ -function loadConfig({ +async function loadConfig({ projectRoot = findProjectRoot(), selectedPlatform, }: { projectRoot?: string; selectedPlatform?: string; -}): Config { +}): Promise { let lazyProject: ProjectConfig; - const userConfig = readConfigFromDisk(projectRoot); + const userConfig = await readConfigFromDisk(projectRoot); const initialConfig: Config = { root: projectRoot, @@ -130,12 +130,13 @@ 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, dependencyName) => { + const acc = await accPromise; const localDependencyRoot = userConfig.dependencies[dependencyName] && userConfig.dependencies[dependencyName].root; @@ -143,7 +144,7 @@ function loadConfig({ let root = localDependencyRoot || resolveNodeModuleDir(projectRoot, dependencyName); - let config = readDependencyConfigFromDisk(root, dependencyName); + let config = await readDependencyConfigFromDisk(root, dependencyName); return assign({}, acc, { dependencies: assign({}, acc.dependencies, { @@ -172,7 +173,7 @@ function loadConfig({ } catch { return acc; } - }, initialConfig); + }, Promise.resolve(initialConfig)); return finalConfig; } diff --git a/packages/cli-config/src/readConfigFromDisk.ts b/packages/cli-config/src/readConfigFromDisk.ts index db47c74fd..18fecbdbf 100644 --- a/packages/cli-config/src/readConfigFromDisk.ts +++ b/packages/cli-config/src/readConfigFromDisk.ts @@ -1,4 +1,4 @@ -import {cosmiconfigSync} from 'cosmiconfig'; +import {cosmiconfig} from 'cosmiconfig'; import {JoiError} from './errors'; import * as schema from './schema'; import { @@ -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 { + 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); @@ -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 { + 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}); diff --git a/packages/cli-doctor/src/commands/doctor.ts b/packages/cli-doctor/src/commands/doctor.ts index a8a2162c3..472768e3b 100644 --- a/packages/cli-doctor/src/commands/doctor.ts +++ b/packages/cli-doctor/src/commands/doctor.ts @@ -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[], ); diff --git a/packages/cli-doctor/src/tools/healthchecks/index.ts b/packages/cli-doctor/src/tools/healthchecks/index.ts index 2dc9f64b7..3cbf1dee2 100644 --- a/packages/cli-doctor/src/tools/healthchecks/index.ts +++ b/packages/cli-doctor/src/tools/healthchecks/index.ts @@ -29,14 +29,16 @@ type Options = { contributor: boolean | void; }; -export const getHealthchecks = ({contributor}: Options): Healthchecks => { +export const getHealthchecks = async ({ + contributor, +}: Options): Promise => { 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) { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d9905c40b..3368b1dd1 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -193,7 +193,7 @@ async function setupAndRun(platformName?: string) { } } - config = loadConfig({ + config = await loadConfig({ selectedPlatform, }); From ebe3911091ea8b030f0a6ba66949f55518f98f47 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 9 Jul 2024 16:32:56 +0200 Subject: [PATCH 2/8] tests: add checks to validate is config is properly received --- __e2e__/config.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index ecd954409..70ac8d893 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -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, @@ -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'); +}); From 6f0db32fd14fb6936387bd4d89ba899e8da24be0 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 9 Jul 2024 16:41:11 +0200 Subject: [PATCH 3/8] fix: units tests --- .../cli-config/src/__tests__/index-test.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/cli-config/src/__tests__/index-test.ts b/packages/cli-config/src/__tests__/index-test.ts index e1c1c1c1b..74fe2dc8c 100644 --- a/packages/cli-config/src/__tests__/index-test.ts +++ b/packages/cli-config/src/__tests__/index-test.ts @@ -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, @@ -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, @@ -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': '{}', @@ -173,7 +173,7 @@ 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'); @@ -181,7 +181,7 @@ test('command specified in root config should overwrite command in "react-native 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, @@ -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: "." }', @@ -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, { @@ -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': `{ @@ -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; @@ -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", @@ -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, @@ -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, @@ -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, @@ -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'; @@ -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( From 0a7e8bf0ba58fa8c3795e6b14741a56fd5cfb790 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Sat, 31 Aug 2024 11:25:12 +0200 Subject: [PATCH 4/8] fix: run each test in fresh environment --- __e2e__/config.test.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index 70ac8d893..a08f57eec 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -35,7 +35,7 @@ function createCorruptedSetupEnvScript() { }; } -beforeAll(() => { +beforeEach(() => { // Clean up folder and re-create a new project cleanup(DIR); writeFiles(DIR, {}); @@ -122,6 +122,20 @@ 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: [ @@ -147,11 +161,11 @@ 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']); - expect(stdout).toBe('test-command'); + 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', () => { From 7f9a3355e4a390fe0decd07317a3e40cb84f3506 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Sat, 31 Aug 2024 12:08:40 +0200 Subject: [PATCH 5/8] test: add proper test scenarios --- __e2e__/config.test.ts | 99 +++++++++++++++++++ packages/cli-config/src/readConfigFromDisk.ts | 2 + 2 files changed, 101 insertions(+) diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index a08f57eec..b49ea296a 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -35,6 +35,13 @@ function createCorruptedSetupEnvScript() { }; } +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); @@ -176,3 +183,95 @@ test('should read user config from react-native.config.mjs', () => { 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', () => { + 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', () => { + 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).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'); +}); diff --git a/packages/cli-config/src/readConfigFromDisk.ts b/packages/cli-config/src/readConfigFromDisk.ts index 18fecbdbf..3f267d660 100644 --- a/packages/cli-config/src/readConfigFromDisk.ts +++ b/packages/cli-config/src/readConfigFromDisk.ts @@ -13,6 +13,8 @@ import chalk from 'chalk'; */ const searchPlaces = [ 'react-native.config.js', + 'react-native.config.cjs', + 'react-native.config.mjs', 'react-native.config.ts', 'react-native.config.mjs', ]; From 770b63722e8e68ea8295d51290cc65917b64929a Mon Sep 17 00:00:00 2001 From: Szymon Rybczak Date: Thu, 31 Oct 2024 09:59:19 +0100 Subject: [PATCH 6/8] Update __e2e__/config.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pierzchała --- __e2e__/config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index b49ea296a..9afa71468 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -184,7 +184,7 @@ test('should read user config from react-native.config.mjs', () => { expect(stdout).toBe('test-command-esm'); }); -test('should fail if if using require() in ES module in react-native.config.mjs', () => { +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'); From d4e8139d6d3fadeee1fdc084be2c14c822330385 Mon Sep 17 00:00:00 2001 From: Szymon Rybczak Date: Thu, 31 Oct 2024 09:59:27 +0100 Subject: [PATCH 7/8] Update __e2e__/config.test.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pierzchała --- __e2e__/config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__e2e__/config.test.ts b/__e2e__/config.test.ts index 9afa71468..322b63829 100644 --- a/__e2e__/config.test.ts +++ b/__e2e__/config.test.ts @@ -201,7 +201,7 @@ test('should fail if using require() in ES module in react-native.config.mjs', ( ); }); -test('should fail if if using require() in ES module with "type": "module" in package.json', () => { +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'); From b9eec5bb9a1c483a8f426882bb53c1d8c1251c35 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Tue, 5 Nov 2024 06:59:13 +0100 Subject: [PATCH 8/8] refactor: create `loadConfigAsync` function --- .../cli-config/src/__tests__/index-test.ts | 28 ++--- packages/cli-config/src/index.ts | 1 + packages/cli-config/src/loadConfig.ts | 109 +++++++++++++++++- packages/cli-config/src/readConfigFromDisk.ts | 65 ++++++++++- .../src/tools/healthchecks/index.ts | 4 +- packages/cli-types/src/index.ts | 2 +- packages/cli/src/index.ts | 6 +- 7 files changed, 187 insertions(+), 28 deletions(-) diff --git a/packages/cli-config/src/__tests__/index-test.ts b/packages/cli-config/src/__tests__/index-test.ts index 74fe2dc8c..c11f6e877 100644 --- a/packages/cli-config/src/__tests__/index-test.ts +++ b/packages/cli-config/src/__tests__/index-test.ts @@ -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'); @@ -66,7 +66,7 @@ test('should have a valid structure by default', async () => { reactNativePath: "." }`, }); - const config = await loadConfig({projectRoot: DIR}); + const config = await loadConfigAsync({projectRoot: DIR}); expect(removeString(config, DIR)).toMatchSnapshot(); }); @@ -83,7 +83,7 @@ test('should return dependencies from package.json', async () => { } }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect(removeString(dependencies, DIR)).toMatchSnapshot(); }); @@ -122,7 +122,7 @@ test('should read a config of a dependency and use it to load other settings', a } }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect( removeString(dependencies['react-native-test'], DIR), ).toMatchSnapshot(); @@ -173,7 +173,7 @@ test('command specified in root config should overwrite command in "react-native ], };`, }); - const {commands} = await loadConfig({projectRoot: DIR}); + const {commands} = await loadConfigAsync({projectRoot: DIR}); const commandsNames = commands.map(({name}) => name); const commandIndex = commandsNames.indexOf('foo-command'); @@ -206,7 +206,7 @@ test('should merge project configuration with default values', async () => { } }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect(removeString(dependencies['react-native-test'], DIR)).toMatchSnapshot( 'snapshoting `react-native-test` config', ); @@ -241,7 +241,7 @@ test('should load commands from "react-native-foo" and "react-native-bar" packag } }`, }); - const {commands} = await loadConfig({projectRoot: DIR}); + const {commands} = await loadConfigAsync({projectRoot: DIR}); expect(commands).toMatchSnapshot(); }); @@ -261,7 +261,7 @@ test('should not skip packages that have invalid configuration (to avoid breakin } }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect(removeString(dependencies, DIR)).toMatchSnapshot( 'dependencies config', ); @@ -281,7 +281,7 @@ test('does not use restricted "react-native" key to resolve config from package. } }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect(dependencies).toHaveProperty('react-native-netinfo'); expect(spy).not.toHaveBeenCalled(); }); @@ -327,7 +327,7 @@ module.exports = { }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect(removeString(dependencies['local-lib'], DIR)).toMatchInlineSnapshot(` Object { "name": "local-lib", @@ -367,7 +367,7 @@ test('should apply build types from dependency config', async () => { } }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect( removeString(dependencies['react-native-test'], DIR), ).toMatchSnapshot(); @@ -400,7 +400,7 @@ test('supports dependencies from user configuration with custom build type', asy }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect( removeString(dependencies['react-native-test'], DIR), ).toMatchSnapshot(); @@ -429,7 +429,7 @@ test('supports disabling dependency for ios platform', async () => { }`, }); - const {dependencies} = await loadConfig({projectRoot: DIR}); + const {dependencies} = await loadConfigAsync({projectRoot: DIR}); expect( removeString(dependencies['react-native-test'], DIR), ).toMatchSnapshot(); @@ -494,7 +494,7 @@ test('should convert project sourceDir relative path to absolute', async () => { `, }); - const config = await 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( diff --git a/packages/cli-config/src/index.ts b/packages/cli-config/src/index.ts index f12851649..6c806cad8 100644 --- a/packages/cli-config/src/index.ts +++ b/packages/cli-config/src/index.ts @@ -1,5 +1,6 @@ import config from './commands/config'; export {default} from './loadConfig'; +export {loadConfigAsync} from './loadConfig'; export const commands = [config]; diff --git a/packages/cli-config/src/loadConfig.ts b/packages/cli-config/src/loadConfig.ts index 82b9a062c..9ebc71ee6 100644 --- a/packages/cli-config/src/loadConfig.ts +++ b/packages/cli-config/src/loadConfig.ts @@ -17,7 +17,9 @@ import findDependencies from './findDependencies'; import resolveReactNativePath from './resolveReactNativePath'; import { readConfigFromDisk, + readConfigFromDiskAsync, readDependencyConfigFromDisk, + readDependencyConfigFromDiskAsync, } from './readConfigFromDisk'; import assign from './assign'; import merge from './merge'; @@ -85,7 +87,103 @@ const removeDuplicateCommands = (commands: Command[]) => { /** * Loads CLI configuration */ -async function loadConfig({ +export default function loadConfig({ + projectRoot = findProjectRoot(), + selectedPlatform, +}: { + projectRoot?: string; + selectedPlatform?: string; +}): Config { + let lazyProject: ProjectConfig; + const userConfig = readConfigFromDisk(projectRoot); + + const initialConfig: Config = { + root: projectRoot, + get reactNativePath() { + return userConfig.reactNativePath + ? path.resolve(projectRoot, userConfig.reactNativePath) + : resolveReactNativePath(projectRoot); + }, + get reactNativeVersion() { + return getReactNativeVersion(initialConfig.reactNativePath); + }, + dependencies: userConfig.dependencies, + commands: userConfig.commands, + healthChecks: userConfig.healthChecks || [], + platforms: userConfig.platforms, + assets: userConfig.assets, + get project() { + if (lazyProject) { + return lazyProject; + } + + lazyProject = {}; + for (const platform in finalConfig.platforms) { + const platformConfig = finalConfig.platforms[platform]; + if (platformConfig) { + lazyProject[platform] = platformConfig.projectConfig( + projectRoot, + userConfig.project[platform] || {}, + ); + } + } + + return lazyProject; + }, + }; + + const finalConfig = Array.from( + new Set([ + ...Object.keys(userConfig.dependencies), + ...findDependencies(projectRoot), + ]), + ).reduce((acc: Config, dependencyName) => { + const localDependencyRoot = + userConfig.dependencies[dependencyName] && + userConfig.dependencies[dependencyName].root; + try { + let root = + localDependencyRoot || + resolveNodeModuleDir(projectRoot, dependencyName); + let config = readDependencyConfigFromDisk(root, dependencyName); + + return assign({}, acc, { + dependencies: assign({}, acc.dependencies, { + get [dependencyName](): DependencyConfig { + return getDependencyConfig( + root, + dependencyName, + finalConfig, + config, + userConfig, + ); + }, + }), + commands: removeDuplicateCommands([ + ...config.commands, + ...acc.commands, + ]), + platforms: { + ...acc.platforms, + ...(selectedPlatform && config.platforms[selectedPlatform] + ? {[selectedPlatform]: config.platforms[selectedPlatform]} + : config.platforms), + }, + healthChecks: [...acc.healthChecks, ...config.healthChecks], + }) as Config; + } catch { + return acc; + } + }, initialConfig); + + return finalConfig; +} + +/** + * Load CLI configuration asynchronously, which supports reading ESM modules. + */ + +export async function loadConfigAsync({ projectRoot = findProjectRoot(), selectedPlatform, }: { @@ -93,7 +191,7 @@ async function loadConfig({ selectedPlatform?: string; }): Promise { let lazyProject: ProjectConfig; - const userConfig = await readConfigFromDisk(projectRoot); + const userConfig = await readConfigFromDiskAsync(projectRoot); const initialConfig: Config = { root: projectRoot, @@ -144,7 +242,10 @@ async function loadConfig({ let root = localDependencyRoot || resolveNodeModuleDir(projectRoot, dependencyName); - let config = await readDependencyConfigFromDisk(root, dependencyName); + let config = await readDependencyConfigFromDiskAsync( + root, + dependencyName, + ); return assign({}, acc, { dependencies: assign({}, acc.dependencies, { @@ -177,5 +278,3 @@ async function loadConfig({ return finalConfig; } - -export default loadConfig; diff --git a/packages/cli-config/src/readConfigFromDisk.ts b/packages/cli-config/src/readConfigFromDisk.ts index 3f267d660..0379dce4e 100644 --- a/packages/cli-config/src/readConfigFromDisk.ts +++ b/packages/cli-config/src/readConfigFromDisk.ts @@ -1,4 +1,4 @@ -import {cosmiconfig} from 'cosmiconfig'; +import {cosmiconfig, cosmiconfigSync} from 'cosmiconfig'; import {JoiError} from './errors'; import * as schema from './schema'; import { @@ -23,7 +23,7 @@ const searchPlaces = [ * Reads a project configuration as defined by the user in the current * workspace. */ -export async function readConfigFromDisk( +export async function readConfigFromDiskAsync( rootFolder: string, ): Promise { const explorer = cosmiconfig('react-native', { @@ -43,11 +43,34 @@ export async function readConfigFromDisk( return result.value as UserConfig; } +/** + * Reads a project configuration as defined by the user in the current + * workspace synchronously. + */ + +export function readConfigFromDisk(rootFolder: string): UserConfig { + const explorer = cosmiconfigSync('react-native', { + stopDir: rootFolder, + searchPlaces, + }); + + const searchResult = explorer.search(rootFolder); + + const config = searchResult ? searchResult.config : undefined; + const result = schema.projectConfig.validate(config); + + if (result.error) { + throw new JoiError(result.error); + } + + return result.value as UserConfig; +} + /** * Reads a dependency configuration as defined by the developer * inside `node_modules`. */ -export async function readDependencyConfigFromDisk( +export async function readDependencyConfigFromDiskAsync( rootFolder: string, dependencyName: string, ): Promise { @@ -78,6 +101,42 @@ export async function readDependencyConfigFromDisk( return result.value as UserDependencyConfig; } +/** + * Reads a dependency configuration as defined by the developer + * inside `node_modules` synchronously. + */ + +export function readDependencyConfigFromDisk( + rootFolder: string, + dependencyName: string, +): UserDependencyConfig { + const explorer = cosmiconfigSync('react-native', { + stopDir: rootFolder, + searchPlaces, + }); + + const searchResult = explorer.search(rootFolder); + const config = searchResult ? searchResult.config : emptyDependencyConfig; + + const result = schema.dependencyConfig.validate(config, {abortEarly: false}); + + if (result.error) { + const validationError = new JoiError(result.error); + logger.warn( + inlineString(` + Package ${chalk.bold( + dependencyName, + )} contains invalid configuration: ${chalk.bold( + validationError.message, + )}. + + Please verify it's properly linked using "npx react-native config" command and contact the package maintainers about this.`), + ); + } + + return result.value as UserDependencyConfig; +} + const emptyDependencyConfig = { dependency: { platforms: {}, diff --git a/packages/cli-doctor/src/tools/healthchecks/index.ts b/packages/cli-doctor/src/tools/healthchecks/index.ts index 3cbf1dee2..52da92141 100644 --- a/packages/cli-doctor/src/tools/healthchecks/index.ts +++ b/packages/cli-doctor/src/tools/healthchecks/index.ts @@ -12,7 +12,7 @@ import xcode from './xcode'; import cocoaPods from './cocoaPods'; import iosDeploy from './iosDeploy'; import {Healthchecks, HealthCheckCategory} from '../../types'; -import loadConfig from '@react-native-community/cli-config'; +import {loadConfigAsync} from '@react-native-community/cli-config'; import xcodeEnv from './xcodeEnv'; import packager from './packager'; import gradle from './gradle'; @@ -38,7 +38,7 @@ export const getHealthchecks = async ({ // Doctor can run in a detached mode, where there isn't a config so this can fail try { - config = await loadConfig({}); + config = await loadConfigAsync({}); additionalChecks = config.healthChecks; if (config.reactNativePath) { diff --git a/packages/cli-types/src/index.ts b/packages/cli-types/src/index.ts index f518e8ebf..1e07ae297 100644 --- a/packages/cli-types/src/index.ts +++ b/packages/cli-types/src/index.ts @@ -59,7 +59,7 @@ export type Command = { export type DetachedCommand = Command; -interface PlatformConfig< +export interface PlatformConfig< ProjectConfig, ProjectParams, DependencyConfig, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 3368b1dd1..4c6e33bc5 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,4 @@ -import loadConfig from '@react-native-community/cli-config'; +import loadConfig, {loadConfigAsync} from '@react-native-community/cli-config'; import {CLIError, logger} from '@react-native-community/cli-tools'; import type { Command, @@ -193,7 +193,7 @@ async function setupAndRun(platformName?: string) { } } - config = await loadConfig({ + config = await loadConfigAsync({ selectedPlatform, }); @@ -243,4 +243,4 @@ async function setupAndRun(platformName?: string) { const bin = require.resolve('./bin'); -export {run, bin, loadConfig}; +export {run, bin, loadConfig, loadConfigAsync};