From f0c1c39df7a611607616b5ba396291bf25aacf50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Sun, 17 Dec 2023 20:53:45 +0100 Subject: [PATCH 01/20] Added inference of watchFolders from NPM/Yarn workspaces --- packages/metro-config/index.js | 52 +++++++++++++++++++++++++++++- packages/metro-config/package.json | 1 + yarn.lock | 11 +++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index 2abf2c8757d475..483be3955742b1 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -10,7 +10,10 @@ /*:: import type {ConfigT} from 'metro-config'; */ +const fastGlob = require('fast-glob'); const {getDefaultConfig: getBaseConfig, mergeConfig} = require('metro-config'); +const fs = require('node:fs'); +const path = require('node:path'); const INTERNAL_CALLSITES_REGEX = new RegExp( [ @@ -36,6 +39,53 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( ].join('|'), ); +/** + * Resolves the root of an NPM or Yarn workspace, by traversing the file tree upwards from a `candidatePath` in the search for + * - a directory with a package.json + * - which has a `workspaces` array of strings + * - which (possibly via a glob) includes the project root + * @param {string} projectRoot Project root to find a workspace root for + * @param {string | undefined} candidatePath Current path to search from + * @returns Path of a workspace root or `undefined` + */ +function getWorkspaceRoot(projectRoot, candidatePath = projectRoot) { + const packageJsonPath = path.resolve(candidatePath, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const { workspaces } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (Array.isArray(workspaces)) { + // If one of the workspaces match the project root, this is the workspace root + // Note: While NPM workspaces doesn't currently support globs, Yarn does. + const matches = fastGlob.sync(workspaces, { + cwd: candidatePath, + onlyDirectories: true, + absolute: true, + }); + if (matches.includes(projectRoot)) { + return candidatePath; + } + } + } catch (err) { + console.warn(`Failed reading or parsing ${packageJsonPath}:`, err); + } + } + // Try one level up + const parentDir = path.dirname(candidatePath); + if (parentDir !== candidatePath) { + return getWorkspaceRoot(projectRoot, parentDir); + } else { + return undefined; + } +} + +/** + * Determine the watch folders + */ +function getWatchFolders(projectRoot) { + const workspaceRoot = getWorkspaceRoot(projectRoot); + return workspaceRoot ? [workspaceRoot] : []; +} + /** * Get the base Metro configuration for a React Native project. */ @@ -82,7 +132,7 @@ function getDefaultConfig( }, }), }, - watchFolders: [], + watchFolders: getWatchFolders(projectRoot), }; // Set global hook so that the CLI can detect when this config has been loaded diff --git a/packages/metro-config/package.json b/packages/metro-config/package.json index 55016cb21ef010..0c09c6e0138968 100644 --- a/packages/metro-config/package.json +++ b/packages/metro-config/package.json @@ -22,6 +22,7 @@ "dependencies": { "@react-native/js-polyfills": "0.74.0", "@react-native/metro-babel-transformer": "0.74.0", + "fast-glob": "^3.3.2", "metro-config": "^0.80.3", "metro-runtime": "^0.80.3" } diff --git a/yarn.lock b/yarn.lock index 0a96c50a30549d..f724d2947cfb25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4954,6 +4954,17 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-patch@^3.0.0-1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" From 5f08c430c1494afcd73b61c1595838489681444c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Mon, 18 Dec 2023 08:32:08 +0100 Subject: [PATCH 02/20] Adding flow types --- packages/metro-config/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index 483be3955742b1..ed73530fb06171 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -11,9 +11,9 @@ /*:: import type {ConfigT} from 'metro-config'; */ const fastGlob = require('fast-glob'); +const fs = require('fs'); const {getDefaultConfig: getBaseConfig, mergeConfig} = require('metro-config'); -const fs = require('node:fs'); -const path = require('node:path'); +const path = require('path'); const INTERNAL_CALLSITES_REGEX = new RegExp( [ @@ -48,7 +48,7 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( * @param {string | undefined} candidatePath Current path to search from * @returns Path of a workspace root or `undefined` */ -function getWorkspaceRoot(projectRoot, candidatePath = projectRoot) { +function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: string */ = projectRoot) /*: string | void */ { const packageJsonPath = path.resolve(candidatePath, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { @@ -81,7 +81,7 @@ function getWorkspaceRoot(projectRoot, candidatePath = projectRoot) { /** * Determine the watch folders */ -function getWatchFolders(projectRoot) { +function getWatchFolders(projectRoot /*: string */) { const workspaceRoot = getWorkspaceRoot(projectRoot); return workspaceRoot ? [workspaceRoot] : []; } From e18e31ffa692eaa10da6273cce9199877d031d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 27 Dec 2023 20:06:18 +0100 Subject: [PATCH 03/20] Apply suggestions from code review Co-authored-by: Rob Hogan <2590098+robhogan@users.noreply.github.com> --- packages/metro-config/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index ed73530fb06171..fc4673555b0242 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -45,12 +45,12 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( * - which has a `workspaces` array of strings * - which (possibly via a glob) includes the project root * @param {string} projectRoot Project root to find a workspace root for - * @param {string | undefined} candidatePath Current path to search from + * @param {string | null | undefined} candidatePath Current path to search from * @returns Path of a workspace root or `undefined` */ -function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: string */ = projectRoot) /*: string | void */ { +function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: ?string */ = projectRoot) /*: ?string */ { const packageJsonPath = path.resolve(candidatePath, 'package.json'); - if (fs.existsSync(packageJsonPath)) { + if (fs.accessSync(packageJsonPath, fs.constants.R_OK)) { try { const { workspaces } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (Array.isArray(workspaces)) { @@ -74,7 +74,7 @@ function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: string */ if (parentDir !== candidatePath) { return getWorkspaceRoot(projectRoot, parentDir); } else { - return undefined; + return null; } } From 626625cfc0b10ee02b4e84ed67f79bd98b7524f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 27 Dec 2023 21:06:46 +0100 Subject: [PATCH 04/20] Using micromatch and removed accessSync guard --- packages/metro-config/index.js | 31 ++++++++++++++---------------- packages/metro-config/package.json | 4 ++-- yarn.lock | 23 ++++++++++------------ 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index fc4673555b0242..d51168b0529fa1 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -10,9 +10,9 @@ /*:: import type {ConfigT} from 'metro-config'; */ -const fastGlob = require('fast-glob'); const fs = require('fs'); const {getDefaultConfig: getBaseConfig, mergeConfig} = require('metro-config'); +const micromatch = require('micromatch'); const path = require('path'); const INTERNAL_CALLSITES_REGEX = new RegExp( @@ -50,22 +50,19 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( */ function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: ?string */ = projectRoot) /*: ?string */ { const packageJsonPath = path.resolve(candidatePath, 'package.json'); - if (fs.accessSync(packageJsonPath, fs.constants.R_OK)) { - try { - const { workspaces } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (Array.isArray(workspaces)) { - // If one of the workspaces match the project root, this is the workspace root - // Note: While NPM workspaces doesn't currently support globs, Yarn does. - const matches = fastGlob.sync(workspaces, { - cwd: candidatePath, - onlyDirectories: true, - absolute: true, - }); - if (matches.includes(projectRoot)) { - return candidatePath; - } + try { + // If the access sync succeeds, it's safe to read the file + const { workspaces } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (Array.isArray(workspaces)) { + // If one of the workspaces match the project root, this is the workspace root + // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch + const relativePath = path.relative(candidatePath, projectRoot); + if (micromatch.isMatch(relativePath, workspaces)) { + return candidatePath; } - } catch (err) { + } + } catch (err) { + if (err.code !== 'ENOENT') { console.warn(`Failed reading or parsing ${packageJsonPath}:`, err); } } @@ -144,4 +141,4 @@ function getDefaultConfig( ); } -module.exports = {getDefaultConfig, mergeConfig}; +module.exports = {getDefaultConfig, mergeConfig, getWorkspaceRoot}; diff --git a/packages/metro-config/package.json b/packages/metro-config/package.json index 0c09c6e0138968..177924be399a92 100644 --- a/packages/metro-config/package.json +++ b/packages/metro-config/package.json @@ -22,8 +22,8 @@ "dependencies": { "@react-native/js-polyfills": "0.74.0", "@react-native/metro-babel-transformer": "0.74.0", - "fast-glob": "^3.3.2", "metro-config": "^0.80.3", - "metro-runtime": "^0.80.3" + "metro-runtime": "^0.80.3", + "micromatch": "^4.0.5" } } diff --git a/yarn.lock b/yarn.lock index f724d2947cfb25..1e9761e28cd365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3633,7 +3633,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -4954,17 +4954,6 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-json-patch@^3.0.0-1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" @@ -7260,6 +7249,14 @@ micromatch@^4.0.4: braces "^3.0.1" picomatch "^2.2.3" +micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + mime-db@1.52.0, "mime-db@>= 1.36.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -7826,7 +7823,7 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== From 4afbf761dacd323b341c73f4b3a52fb7154bb58e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 27 Dec 2023 21:06:53 +0100 Subject: [PATCH 05/20] Adding tests --- .../__tests__/get-workspace-root-test.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/metro-config/__tests__/get-workspace-root-test.js diff --git a/packages/metro-config/__tests__/get-workspace-root-test.js b/packages/metro-config/__tests__/get-workspace-root-test.js new file mode 100644 index 00000000000000..41b52d7c1a273e --- /dev/null +++ b/packages/metro-config/__tests__/get-workspace-root-test.js @@ -0,0 +1,70 @@ +const { getWorkspaceRoot } = require('..'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +function createTempPackage(packageJson, packagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-metro-config-test-'))) { + fs.mkdirSync(packagePath, { recursive: true }); + if (typeof packageJson === 'object') { + fs.writeFileSync(path.join(packagePath, 'package.json'), JSON.stringify(packageJson), 'utf8'); + } + return packagePath; +} + +describe('getWorkspaceRoot', () => { + test('returns null if not in a workspace', () => { + const tempPackagePath = createTempPackage({ + name: 'my-app', + }); + expect(getWorkspaceRoot(tempPackagePath)).toBe(null); + }); + + test('supports an NPM workspace', () => { + const tempWorkspaceRootPath = createTempPackage({ + name: 'package-root', + workspaces: ['packages/my-app', 'packages/my-lib'], + }); + const tempPackagePath = createTempPackage({ + name: 'my-app', + }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); + }); + + test('supports a Yarn workspace', () => { + const tempWorkspaceRootPath = createTempPackage({ + name: 'package-root', + workspaces: ['packages/*'], + }); + const tempPackagePath = createTempPackage({ + name: 'my-app', + }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); + }); + + test.skip('supports a pnpm workspace', () => { + const tempWorkspaceRootPath = createTempPackage({ + name: 'package-root', + }); + // Create the pnpm workspace configuration (see https://pnpm.io/pnpm-workspace_yaml) + const workspacesConfig = 'packages: ["packages/*"]'; + fs.writeFileSync(path.join(tempWorkspaceRootPath, 'pnpm-workspace.yaml'), workspacesConfig, 'utf8'); + const tempPackagePath = createTempPackage({ + name: 'my-app', + }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); + }); + + test.skip('supports a pnpm workspace exclusion', () => { + const tempWorkspaceRootPath = createTempPackage({ + name: 'package-root', + }); + // Create the pnpm workspace configuration (see https://pnpm.io/pnpm-workspace_yaml) + const workspacesConfig = 'packages: ["packages/*", "!packages/*-app"]'; + fs.writeFileSync(path.join(tempWorkspaceRootPath, 'pnpm-workspace.yaml'), workspacesConfig, 'utf8'); + const tempPackagePath = createTempPackage({ + name: 'my-app', + }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + expect(getWorkspaceRoot(tempPackagePath)).toBe(null); + }); +}); + From f27979c88218a6751af289f7eaabb5564fa8731c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 27 Dec 2023 21:10:15 +0100 Subject: [PATCH 06/20] Fixed flow type --- packages/metro-config/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index d51168b0529fa1..91b75c5f5e1d5d 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -48,7 +48,7 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( * @param {string | null | undefined} candidatePath Current path to search from * @returns Path of a workspace root or `undefined` */ -function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: ?string */ = projectRoot) /*: ?string */ { +function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: string */ = projectRoot) /*: ?string */ { const packageJsonPath = path.resolve(candidatePath, 'package.json'); try { // If the access sync succeeds, it's safe to read the file From 395b2d9d56898572957d2deb267dab525c54068c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 28 Dec 2023 19:56:18 +0100 Subject: [PATCH 07/20] Moved code to the cli-plugin package --- packages/community-cli-plugin/package.json | 1 + .../utils/__tests__/getWorkspaceRoot-test.js} | 2 +- .../src/utils/getWorkspaceRoot.js | 50 ++++++++++++++++++ packages/metro-config/index.js | 51 +------------------ packages/metro-config/package.json | 3 +- 5 files changed, 55 insertions(+), 52 deletions(-) rename packages/{metro-config/__tests__/get-workspace-root-test.js => community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js} (97%) create mode 100644 packages/community-cli-plugin/src/utils/getWorkspaceRoot.js diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 20a2be8d5fd6a7..580f3263b51e7f 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -31,6 +31,7 @@ "metro": "^0.80.3", "metro-config": "^0.80.3", "metro-core": "^0.80.3", + "micromatch": "^4.0.5", "node-fetch": "^2.2.0", "querystring": "^0.2.1", "readline": "^1.3.0" diff --git a/packages/metro-config/__tests__/get-workspace-root-test.js b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js similarity index 97% rename from packages/metro-config/__tests__/get-workspace-root-test.js rename to packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js index 41b52d7c1a273e..6c41c521c87e42 100644 --- a/packages/metro-config/__tests__/get-workspace-root-test.js +++ b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js @@ -1,4 +1,4 @@ -const { getWorkspaceRoot } = require('..'); +const { getWorkspaceRoot } = require('../getWorkspaceRoot'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js new file mode 100644 index 00000000000000..8fa380cefd4f0d --- /dev/null +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -0,0 +1,50 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +const fs = require('fs'); +const micromatch = require('micromatch'); +const path = require('path'); + +/** + * Resolves the root of an NPM or Yarn workspace, by traversing the file tree upwards from a `candidatePath` in the search for + * - a directory with a package.json + * - which has a `workspaces` array of strings + * - which (possibly via a glob) includes the project root + */ +export function getWorkspaceRoot( + projectRoot /*: string */, + candidatePath /*: string */ = projectRoot, +) /*: ?string */ { + const packageJsonPath = path.resolve(candidatePath, 'package.json'); + try { + // If the access sync succeeds, it's safe to read the file + const {workspaces} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (Array.isArray(workspaces)) { + // If one of the workspaces match the project root, this is the workspace root + // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch + const relativePath = path.relative(candidatePath, projectRoot); + if (micromatch.isMatch(relativePath, workspaces)) { + return candidatePath; + } + } + } catch (err) { + if (err.code !== 'ENOENT') { + console.warn(`Failed reading or parsing ${packageJsonPath}:`, err); + } + } + // Try one level up + const parentDir = path.dirname(candidatePath); + if (parentDir !== candidatePath) { + return getWorkspaceRoot(projectRoot, parentDir); + } else { + return null; + } +} diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index 91b75c5f5e1d5d..2abf2c8757d475 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -10,10 +10,7 @@ /*:: import type {ConfigT} from 'metro-config'; */ -const fs = require('fs'); const {getDefaultConfig: getBaseConfig, mergeConfig} = require('metro-config'); -const micromatch = require('micromatch'); -const path = require('path'); const INTERNAL_CALLSITES_REGEX = new RegExp( [ @@ -39,50 +36,6 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( ].join('|'), ); -/** - * Resolves the root of an NPM or Yarn workspace, by traversing the file tree upwards from a `candidatePath` in the search for - * - a directory with a package.json - * - which has a `workspaces` array of strings - * - which (possibly via a glob) includes the project root - * @param {string} projectRoot Project root to find a workspace root for - * @param {string | null | undefined} candidatePath Current path to search from - * @returns Path of a workspace root or `undefined` - */ -function getWorkspaceRoot(projectRoot /*: string */, candidatePath /*: string */ = projectRoot) /*: ?string */ { - const packageJsonPath = path.resolve(candidatePath, 'package.json'); - try { - // If the access sync succeeds, it's safe to read the file - const { workspaces } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (Array.isArray(workspaces)) { - // If one of the workspaces match the project root, this is the workspace root - // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch - const relativePath = path.relative(candidatePath, projectRoot); - if (micromatch.isMatch(relativePath, workspaces)) { - return candidatePath; - } - } - } catch (err) { - if (err.code !== 'ENOENT') { - console.warn(`Failed reading or parsing ${packageJsonPath}:`, err); - } - } - // Try one level up - const parentDir = path.dirname(candidatePath); - if (parentDir !== candidatePath) { - return getWorkspaceRoot(projectRoot, parentDir); - } else { - return null; - } -} - -/** - * Determine the watch folders - */ -function getWatchFolders(projectRoot /*: string */) { - const workspaceRoot = getWorkspaceRoot(projectRoot); - return workspaceRoot ? [workspaceRoot] : []; -} - /** * Get the base Metro configuration for a React Native project. */ @@ -129,7 +82,7 @@ function getDefaultConfig( }, }), }, - watchFolders: getWatchFolders(projectRoot), + watchFolders: [], }; // Set global hook so that the CLI can detect when this config has been loaded @@ -141,4 +94,4 @@ function getDefaultConfig( ); } -module.exports = {getDefaultConfig, mergeConfig, getWorkspaceRoot}; +module.exports = {getDefaultConfig, mergeConfig}; diff --git a/packages/metro-config/package.json b/packages/metro-config/package.json index 177924be399a92..55016cb21ef010 100644 --- a/packages/metro-config/package.json +++ b/packages/metro-config/package.json @@ -23,7 +23,6 @@ "@react-native/js-polyfills": "0.74.0", "@react-native/metro-babel-transformer": "0.74.0", "metro-config": "^0.80.3", - "metro-runtime": "^0.80.3", - "micromatch": "^4.0.5" + "metro-runtime": "^0.80.3" } } From 6c91b224898f387898cdaf57bc67a32f72aa5ae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 28 Dec 2023 20:48:56 +0100 Subject: [PATCH 08/20] Using getWorkspaceRoot to supply watchFolders as a fallback --- .../community-cli-plugin/src/utils/getWorkspaceRoot.js | 2 +- .../community-cli-plugin/src/utils/loadMetroConfig.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js index 8fa380cefd4f0d..c2d677722f33f7 100644 --- a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @flow strict-local + * @flow * @format * @oncall react_native */ diff --git a/packages/community-cli-plugin/src/utils/loadMetroConfig.js b/packages/community-cli-plugin/src/utils/loadMetroConfig.js index 44d0d275cb0e64..d0befb3785bb58 100644 --- a/packages/community-cli-plugin/src/utils/loadMetroConfig.js +++ b/packages/community-cli-plugin/src/utils/loadMetroConfig.js @@ -12,6 +12,7 @@ import type {Config} from '@react-native-community/cli-types'; import type {ConfigT, InputConfigT, YargArguments} from 'metro-config'; +import {getWorkspaceRoot} from './getWorkspaceRoot'; import {reactNativePlatformResolver} from './metroPlatformResolver'; import {CLIError, logger} from '@react-native-community/cli-tools'; import {loadConfig, mergeConfig, resolveConfig} from 'metro-config'; @@ -26,6 +27,11 @@ export type ConfigLoadingContext = $ReadOnly<{ ... }>; +function getWatchFolders(projectRoot: string) { + const workspaceRoot = getWorkspaceRoot(projectRoot); + return typeof workspaceRoot === 'string' ? [workspaceRoot] : undefined; +} + /** * Get the config options to override based on RN CLI inputs. */ @@ -70,6 +76,10 @@ function getOverrideConfig( ), ], }, + watchFolders: + typeof config.watchFolders === 'undefined' + ? getWatchFolders(ctx.root) + : undefined, }; } From e4b8f9b21011ff2eeb314f2620cc4e4ab00e7d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 28 Dec 2023 21:45:37 +0100 Subject: [PATCH 09/20] Provide the watchFolders override property only if project had no watch folders --- .../src/utils/loadMetroConfig.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/loadMetroConfig.js b/packages/community-cli-plugin/src/utils/loadMetroConfig.js index d0befb3785bb58..de75e8565b27f7 100644 --- a/packages/community-cli-plugin/src/utils/loadMetroConfig.js +++ b/packages/community-cli-plugin/src/utils/loadMetroConfig.js @@ -59,7 +59,7 @@ function getOverrideConfig( ); } - return { + const overrides: InputConfigT = { resolver, serializer: { // We can include multiple copies of InitializeCore here because metro will @@ -76,11 +76,13 @@ function getOverrideConfig( ), ], }, - watchFolders: - typeof config.watchFolders === 'undefined' - ? getWatchFolders(ctx.root) - : undefined, }; + + if (config.watchFolders.length === 0) { + overrides.watchFolders = getWatchFolders(ctx.root); + } + + return overrides; } /** From 9f93c7b0ebac3ccafc942fb4a883dbe2727054d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 28 Dec 2023 22:25:45 +0100 Subject: [PATCH 10/20] Appending workspace root as watch folder only if not other folder than the project root was given --- .../src/utils/loadMetroConfig.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/loadMetroConfig.js b/packages/community-cli-plugin/src/utils/loadMetroConfig.js index de75e8565b27f7..b233b873252c62 100644 --- a/packages/community-cli-plugin/src/utils/loadMetroConfig.js +++ b/packages/community-cli-plugin/src/utils/loadMetroConfig.js @@ -27,11 +27,6 @@ export type ConfigLoadingContext = $ReadOnly<{ ... }>; -function getWatchFolders(projectRoot: string) { - const workspaceRoot = getWorkspaceRoot(projectRoot); - return typeof workspaceRoot === 'string' ? [workspaceRoot] : undefined; -} - /** * Get the config options to override based on RN CLI inputs. */ @@ -78,8 +73,13 @@ function getOverrideConfig( }, }; - if (config.watchFolders.length === 0) { - overrides.watchFolders = getWatchFolders(ctx.root); + // Applying the heuristic of appending workspace root as watch folder, + // only if no other watch folder (beside the project root) has been given. + if (!config.watchFolders.some(folder => folder !== ctx.root)) { + const workspaceRoot = getWorkspaceRoot(ctx.root); + if (typeof workspaceRoot === 'string') { + overrides.watchFolders = [...config.watchFolders, workspaceRoot]; + } } return overrides; From 43594bd23badad98e35a8357a54e13481771340a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 29 Dec 2023 20:16:44 +0100 Subject: [PATCH 11/20] Adding support for an alternative way for Yarn to declare workspaces --- .../utils/__tests__/getWorkspaceRoot-test.js | 13 +++++++ .../src/utils/getWorkspaceRoot.js | 38 +++++++++++++------ 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js index 6c41c521c87e42..f42337d85277a7 100644 --- a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js +++ b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js @@ -41,6 +41,19 @@ describe('getWorkspaceRoot', () => { expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); + test('supports a Yarn workspace (object style)', () => { + const tempWorkspaceRootPath = createTempPackage({ + name: 'package-root', + workspaces: { + packages: ['packages/*'], + }, + }); + const tempPackagePath = createTempPackage({ + name: 'my-app', + }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); + }); + test.skip('supports a pnpm workspace', () => { const tempWorkspaceRootPath = createTempPackage({ name: 'package-root', diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js index c2d677722f33f7..2e5fd3f1aa4fab 100644 --- a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -13,6 +13,26 @@ const fs = require('fs'); const micromatch = require('micromatch'); const path = require('path'); +/** + * Get the workspace paths from the path of a potential workspace root. + */ +function getWorkspacePaths(packagePath /*: string */) /*: string[] */ { + const packageJsonPath = path.resolve(packagePath, 'package.json'); + const {workspaces} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (Array.isArray(workspaces)) { + return workspaces; + } else if ( + typeof workspaces === 'object' && + Array.isArray(workspaces.packages) + ) { + // An alternative way for Yarn to declare workspace packages + return workspaces.packages; + } else { + return []; + } + // TODO: Support PNPN workspaces +} + /** * Resolves the root of an NPM or Yarn workspace, by traversing the file tree upwards from a `candidatePath` in the search for * - a directory with a package.json @@ -23,21 +43,17 @@ export function getWorkspaceRoot( projectRoot /*: string */, candidatePath /*: string */ = projectRoot, ) /*: ?string */ { - const packageJsonPath = path.resolve(candidatePath, 'package.json'); try { - // If the access sync succeeds, it's safe to read the file - const {workspaces} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (Array.isArray(workspaces)) { - // If one of the workspaces match the project root, this is the workspace root - // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch - const relativePath = path.relative(candidatePath, projectRoot); - if (micromatch.isMatch(relativePath, workspaces)) { - return candidatePath; - } + const workspacePaths = getWorkspacePaths(candidatePath); + // If one of the workspaces match the project root, this is the workspace root + // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch + const relativePath = path.relative(candidatePath, projectRoot); + if (micromatch.isMatch(relativePath, workspacePaths)) { + return candidatePath; } } catch (err) { if (err.code !== 'ENOENT') { - console.warn(`Failed reading or parsing ${packageJsonPath}:`, err); + console.warn(`Failed getting workspace root from ${candidatePath}:`, err); } } // Try one level up From 8a734bfd0788bb793d2942b09c262773376c34d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Fri, 29 Dec 2023 21:01:38 +0100 Subject: [PATCH 12/20] Adding support for PNPM workspaces --- packages/community-cli-plugin/package.json | 3 +- .../utils/__tests__/getWorkspaceRoot-test.js | 4 +- .../src/utils/getWorkspaceRoot.js | 68 ++++++++++++------- yarn.lock | 5 ++ 4 files changed, 52 insertions(+), 28 deletions(-) diff --git a/packages/community-cli-plugin/package.json b/packages/community-cli-plugin/package.json index 580f3263b51e7f..42ac73ae2a6240 100644 --- a/packages/community-cli-plugin/package.json +++ b/packages/community-cli-plugin/package.json @@ -34,7 +34,8 @@ "micromatch": "^4.0.5", "node-fetch": "^2.2.0", "querystring": "^0.2.1", - "readline": "^1.3.0" + "readline": "^1.3.0", + "yaml": "^2.3.4" }, "devDependencies": { "metro-resolver": "^0.80.3" diff --git a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js index f42337d85277a7..3bdc5c0312be68 100644 --- a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js +++ b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js @@ -54,7 +54,7 @@ describe('getWorkspaceRoot', () => { expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); - test.skip('supports a pnpm workspace', () => { + test('supports a pnpm workspace', () => { const tempWorkspaceRootPath = createTempPackage({ name: 'package-root', }); @@ -67,7 +67,7 @@ describe('getWorkspaceRoot', () => { expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); - test.skip('supports a pnpm workspace exclusion', () => { + test('supports a pnpm workspace exclusion', () => { const tempWorkspaceRootPath = createTempPackage({ name: 'package-root', }); diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js index 2e5fd3f1aa4fab..34061521f70f9f 100644 --- a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -12,25 +12,48 @@ const fs = require('fs'); const micromatch = require('micromatch'); const path = require('path'); +const yaml = require('yaml'); /** * Get the workspace paths from the path of a potential workspace root. + * + * This supports: + * - [NPM workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces) + * - [Yarn workspaces](https://yarnpkg.com/features/workspaces) + * - [PNPM workspaces](https://pnpm.io/workspaces) */ function getWorkspacePaths(packagePath /*: string */) /*: string[] */ { - const packageJsonPath = path.resolve(packagePath, 'package.json'); - const {workspaces} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (Array.isArray(workspaces)) { - return workspaces; - } else if ( - typeof workspaces === 'object' && - Array.isArray(workspaces.packages) - ) { - // An alternative way for Yarn to declare workspace packages - return workspaces.packages; - } else { - return []; + const result /*: string[] */ = []; + try { + const packageJsonPath = path.resolve(packagePath, 'package.json'); + const {workspaces} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (Array.isArray(workspaces)) { + result.push(...workspaces); + } else if ( + typeof workspaces === 'object' && + Array.isArray(workspaces.packages) + ) { + // An alternative way for Yarn to declare workspace packages + result.push(...workspaces.packages); + } + // Falling back to PNPN workspaces + const pnpmWorkspacePath = path.resolve(packagePath, 'pnpm-workspace.yaml'); + const pnpmWorkspaceConfig = yaml.parse( + fs.readFileSync(pnpmWorkspacePath, 'utf8'), + ); + if ( + typeof pnpmWorkspaceConfig === 'object' && + Array.isArray(pnpmWorkspaceConfig.packages) + ) { + result.push(...pnpmWorkspaceConfig.packages); + } + } catch (err) { + if (err.code !== 'ENOENT') { + console.warn(`Failed getting workspace root from ${packagePath}:`, err); + } + } finally { + return result; } - // TODO: Support PNPN workspaces } /** @@ -43,18 +66,13 @@ export function getWorkspaceRoot( projectRoot /*: string */, candidatePath /*: string */ = projectRoot, ) /*: ?string */ { - try { - const workspacePaths = getWorkspacePaths(candidatePath); - // If one of the workspaces match the project root, this is the workspace root - // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch - const relativePath = path.relative(candidatePath, projectRoot); - if (micromatch.isMatch(relativePath, workspacePaths)) { - return candidatePath; - } - } catch (err) { - if (err.code !== 'ENOENT') { - console.warn(`Failed getting workspace root from ${candidatePath}:`, err); - } + const workspacePaths = getWorkspacePaths(candidatePath); + // If one of the workspaces match the project root, this is the workspace root + // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch + const relativePath = path.relative(candidatePath, projectRoot); + // Using this instead of `micromatch.isMatch` to enable excluding patterns + if (micromatch([relativePath], workspacePaths).length > 0) { + return candidatePath; } // Try one level up const parentDir = path.dirname(candidatePath); diff --git a/yarn.lock b/yarn.lock index 1e9761e28cd365..ef916208636458 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9594,6 +9594,11 @@ yaml@^2.2.1: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== +yaml@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" From 2059ac2ce7a4f2e915805e921c7c3c437d1ae790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 2 Jan 2024 15:27:45 +0100 Subject: [PATCH 13/20] Apply suggestions from code review Co-authored-by: Alex Hunt --- .../community-cli-plugin/src/utils/getWorkspaceRoot.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js index 34061521f70f9f..86c951312ae13c 100644 --- a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -22,7 +22,7 @@ const yaml = require('yaml'); * - [Yarn workspaces](https://yarnpkg.com/features/workspaces) * - [PNPM workspaces](https://pnpm.io/workspaces) */ -function getWorkspacePaths(packagePath /*: string */) /*: string[] */ { +function getWorkspacePaths(packagePath: string): Array { const result /*: string[] */ = []; try { const packageJsonPath = path.resolve(packagePath, 'package.json'); @@ -57,7 +57,8 @@ function getWorkspacePaths(packagePath /*: string */) /*: string[] */ { } /** - * Resolves the root of an NPM or Yarn workspace, by traversing the file tree upwards from a `candidatePath` in the search for + * Resolves the root of an npm or Yarn workspace, by traversing the file tree + * upwards from a `candidatePath` in the search for * - a directory with a package.json * - which has a `workspaces` array of strings * - which (possibly via a glob) includes the project root @@ -78,7 +79,6 @@ export function getWorkspaceRoot( const parentDir = path.dirname(candidatePath); if (parentDir !== candidatePath) { return getWorkspaceRoot(projectRoot, parentDir); - } else { - return null; } + return null; } From eca58a6f15543aa01798e3dae3f291cfea58725d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 2 Jan 2024 15:39:36 +0100 Subject: [PATCH 14/20] Turned test file to flow --- .../utils/__tests__/getWorkspaceRoot-test.js | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js index 3bdc5c0312be68..1eb20cf7ceda01 100644 --- a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js +++ b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js @@ -1,12 +1,32 @@ -const { getWorkspaceRoot } = require('../getWorkspaceRoot'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ -function createTempPackage(packageJson, packagePath = fs.mkdtempSync(path.join(os.tmpdir(), 'rn-metro-config-test-'))) { - fs.mkdirSync(packagePath, { recursive: true }); +const {getWorkspaceRoot} = require('../getWorkspaceRoot'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function createTempPackage( + packageJson: {...}, + packagePath: string = fs.mkdtempSync( + path.join(os.tmpdir(), 'rn-metro-config-test-'), + ), +) { + fs.mkdirSync(packagePath, {recursive: true}); if (typeof packageJson === 'object') { - fs.writeFileSync(path.join(packagePath, 'package.json'), JSON.stringify(packageJson), 'utf8'); + fs.writeFileSync( + path.join(packagePath, 'package.json'), + JSON.stringify(packageJson), + 'utf8', + ); } return packagePath; } @@ -24,9 +44,12 @@ describe('getWorkspaceRoot', () => { name: 'package-root', workspaces: ['packages/my-app', 'packages/my-lib'], }); - const tempPackagePath = createTempPackage({ - name: 'my-app', - }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + const tempPackagePath = createTempPackage( + { + name: 'my-app', + }, + path.join(tempWorkspaceRootPath, 'packages', 'my-app'), + ); expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); @@ -35,9 +58,12 @@ describe('getWorkspaceRoot', () => { name: 'package-root', workspaces: ['packages/*'], }); - const tempPackagePath = createTempPackage({ - name: 'my-app', - }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + const tempPackagePath = createTempPackage( + { + name: 'my-app', + }, + path.join(tempWorkspaceRootPath, 'packages', 'my-app'), + ); expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); @@ -48,9 +74,12 @@ describe('getWorkspaceRoot', () => { packages: ['packages/*'], }, }); - const tempPackagePath = createTempPackage({ - name: 'my-app', - }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + const tempPackagePath = createTempPackage( + { + name: 'my-app', + }, + path.join(tempWorkspaceRootPath, 'packages', 'my-app'), + ); expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); @@ -60,10 +89,17 @@ describe('getWorkspaceRoot', () => { }); // Create the pnpm workspace configuration (see https://pnpm.io/pnpm-workspace_yaml) const workspacesConfig = 'packages: ["packages/*"]'; - fs.writeFileSync(path.join(tempWorkspaceRootPath, 'pnpm-workspace.yaml'), workspacesConfig, 'utf8'); - const tempPackagePath = createTempPackage({ - name: 'my-app', - }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + fs.writeFileSync( + path.join(tempWorkspaceRootPath, 'pnpm-workspace.yaml'), + workspacesConfig, + 'utf8', + ); + const tempPackagePath = createTempPackage( + { + name: 'my-app', + }, + path.join(tempWorkspaceRootPath, 'packages', 'my-app'), + ); expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); @@ -73,11 +109,17 @@ describe('getWorkspaceRoot', () => { }); // Create the pnpm workspace configuration (see https://pnpm.io/pnpm-workspace_yaml) const workspacesConfig = 'packages: ["packages/*", "!packages/*-app"]'; - fs.writeFileSync(path.join(tempWorkspaceRootPath, 'pnpm-workspace.yaml'), workspacesConfig, 'utf8'); - const tempPackagePath = createTempPackage({ - name: 'my-app', - }, path.join(tempWorkspaceRootPath, 'packages', 'my-app')); + fs.writeFileSync( + path.join(tempWorkspaceRootPath, 'pnpm-workspace.yaml'), + workspacesConfig, + 'utf8', + ); + const tempPackagePath = createTempPackage( + { + name: 'my-app', + }, + path.join(tempWorkspaceRootPath, 'packages', 'my-app'), + ); expect(getWorkspaceRoot(tempPackagePath)).toBe(null); }); }); - From 257a412bea44c3f52f95139bad9722d7f075644c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 2 Jan 2024 16:01:56 +0100 Subject: [PATCH 15/20] Apply more suggestions from code review --- .../utils/__tests__/getWorkspaceRoot-test.js | 14 ++--- .../src/utils/getWorkspaceRoot.js | 56 ++++++++++--------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js index 1eb20cf7ceda01..dd25247bc10823 100644 --- a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js +++ b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js @@ -9,10 +9,10 @@ * @oncall react_native */ -const {getWorkspaceRoot} = require('../getWorkspaceRoot'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); +import {getWorkspaceRoot} from '../getWorkspaceRoot'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; function createTempPackage( packageJson: {...}, @@ -39,7 +39,7 @@ describe('getWorkspaceRoot', () => { expect(getWorkspaceRoot(tempPackagePath)).toBe(null); }); - test('supports an NPM workspace', () => { + test('supports an npm workspace', () => { const tempWorkspaceRootPath = createTempPackage({ name: 'package-root', workspaces: ['packages/my-app', 'packages/my-lib'], @@ -53,7 +53,7 @@ describe('getWorkspaceRoot', () => { expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); - test('supports a Yarn workspace', () => { + test('supports a yarn workspace', () => { const tempWorkspaceRootPath = createTempPackage({ name: 'package-root', workspaces: ['packages/*'], @@ -67,7 +67,7 @@ describe('getWorkspaceRoot', () => { expect(getWorkspaceRoot(tempPackagePath)).toBe(tempWorkspaceRootPath); }); - test('supports a Yarn workspace (object style)', () => { + test('supports a yarn workspace (object style)', () => { const tempWorkspaceRootPath = createTempPackage({ name: 'package-root', workspaces: { diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js index 86c951312ae13c..43153ba9ccd2f9 100644 --- a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -9,55 +9,57 @@ * @oncall react_native */ -const fs = require('fs'); -const micromatch = require('micromatch'); -const path = require('path'); -const yaml = require('yaml'); +import {logger} from '@react-native-community/cli-tools'; +import fs from 'fs'; +import micromatch from 'micromatch'; +import path from 'path'; +import yaml from 'yaml'; /** * Get the workspace paths from the path of a potential workspace root. * * This supports: - * - [NPM workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces) - * - [Yarn workspaces](https://yarnpkg.com/features/workspaces) - * - [PNPM workspaces](https://pnpm.io/workspaces) + * - [npm workspaces](https://docs.npmjs.com/cli/v10/using-npm/workspaces) + * - [yarn workspaces](https://yarnpkg.com/features/workspaces) + * - [pnpm workspaces](https://pnpm.io/workspaces) */ function getWorkspacePaths(packagePath: string): Array { - const result /*: string[] */ = []; try { + // Checking pnpm workspaces first + const pnpmWorkspacePath = path.resolve(packagePath, 'pnpm-workspace.yaml'); + if (fs.existsSync(pnpmWorkspacePath)) { + const pnpmWorkspaceConfig = yaml.parse( + fs.readFileSync(pnpmWorkspacePath, 'utf8'), + ); + if ( + typeof pnpmWorkspaceConfig === 'object' && + Array.isArray(pnpmWorkspaceConfig.packages) + ) { + return pnpmWorkspaceConfig.packages; + } + } + // Falling back to npm / yarn workspaces const packageJsonPath = path.resolve(packagePath, 'package.json'); const {workspaces} = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (Array.isArray(workspaces)) { - result.push(...workspaces); + return workspaces; } else if ( typeof workspaces === 'object' && Array.isArray(workspaces.packages) ) { - // An alternative way for Yarn to declare workspace packages - result.push(...workspaces.packages); - } - // Falling back to PNPN workspaces - const pnpmWorkspacePath = path.resolve(packagePath, 'pnpm-workspace.yaml'); - const pnpmWorkspaceConfig = yaml.parse( - fs.readFileSync(pnpmWorkspacePath, 'utf8'), - ); - if ( - typeof pnpmWorkspaceConfig === 'object' && - Array.isArray(pnpmWorkspaceConfig.packages) - ) { - result.push(...pnpmWorkspaceConfig.packages); + // An alternative way for yarn to declare workspace packages + return workspaces.packages; } } catch (err) { if (err.code !== 'ENOENT') { - console.warn(`Failed getting workspace root from ${packagePath}:`, err); + logger.debug(`Failed getting workspace root from ${packagePath}: ${err}`); } - } finally { - return result; } + return []; } /** - * Resolves the root of an npm or Yarn workspace, by traversing the file tree + * Resolves the root of an npm or yarn workspace, by traversing the file tree * upwards from a `candidatePath` in the search for * - a directory with a package.json * - which has a `workspaces` array of strings @@ -69,7 +71,7 @@ export function getWorkspaceRoot( ) /*: ?string */ { const workspacePaths = getWorkspacePaths(candidatePath); // If one of the workspaces match the project root, this is the workspace root - // Note: While NPM workspaces doesn't currently support globs, Yarn does, which is why we use micromatch + // Note: While npm workspaces doesn't currently support globs, yarn does, which is why we use micromatch const relativePath = path.relative(candidatePath, projectRoot); // Using this instead of `micromatch.isMatch` to enable excluding patterns if (micromatch([relativePath], workspacePaths).length > 0) { From 60b6a836fc64cdd484639dfdd2238b6d5722ad0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Tue, 2 Jan 2024 16:16:14 +0100 Subject: [PATCH 16/20] Using more flow without comments --- packages/community-cli-plugin/src/utils/getWorkspaceRoot.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js index 43153ba9ccd2f9..3101adde23816d 100644 --- a/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js +++ b/packages/community-cli-plugin/src/utils/getWorkspaceRoot.js @@ -66,9 +66,9 @@ function getWorkspacePaths(packagePath: string): Array { * - which (possibly via a glob) includes the project root */ export function getWorkspaceRoot( - projectRoot /*: string */, - candidatePath /*: string */ = projectRoot, -) /*: ?string */ { + projectRoot: string, + candidatePath: string = projectRoot, +): ?string { const workspacePaths = getWorkspacePaths(candidatePath); // If one of the workspaces match the project root, this is the workspace root // Note: While npm workspaces doesn't currently support globs, yarn does, which is why we use micromatch From d63b235a7113bb3198e6a4006a902fc7acd41fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 1 Feb 2024 22:07:50 +0100 Subject: [PATCH 17/20] Implemented a failing test --- .../utils/__tests__/getWorkspaceRoot-test.js | 19 +-- .../utils/__tests__/loadMetroConfig-test.js | 133 ++++++++++++++++++ .../src/utils/__tests__/temporary-package.js | 33 +++++ 3 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 packages/community-cli-plugin/src/utils/__tests__/loadMetroConfig-test.js create mode 100644 packages/community-cli-plugin/src/utils/__tests__/temporary-package.js diff --git a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js index dd25247bc10823..6d73456d9b2c4a 100644 --- a/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js +++ b/packages/community-cli-plugin/src/utils/__tests__/getWorkspaceRoot-test.js @@ -10,27 +10,10 @@ */ import {getWorkspaceRoot} from '../getWorkspaceRoot'; +import {createTempPackage} from './temporary-package'; import fs from 'fs'; -import os from 'os'; import path from 'path'; -function createTempPackage( - packageJson: {...}, - packagePath: string = fs.mkdtempSync( - path.join(os.tmpdir(), 'rn-metro-config-test-'), - ), -) { - fs.mkdirSync(packagePath, {recursive: true}); - if (typeof packageJson === 'object') { - fs.writeFileSync( - path.join(packagePath, 'package.json'), - JSON.stringify(packageJson), - 'utf8', - ); - } - return packagePath; -} - describe('getWorkspaceRoot', () => { test('returns null if not in a workspace', () => { const tempPackagePath = createTempPackage({ diff --git a/packages/community-cli-plugin/src/utils/__tests__/loadMetroConfig-test.js b/packages/community-cli-plugin/src/utils/__tests__/loadMetroConfig-test.js new file mode 100644 index 00000000000000..4610e874cb5192 --- /dev/null +++ b/packages/community-cli-plugin/src/utils/__tests__/loadMetroConfig-test.js @@ -0,0 +1,133 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import loadMetroConfig from '../loadMetroConfig'; +import {createTempPackage} from './temporary-package'; +import fs from 'fs'; +import path from 'path'; + +/** + * Resolves a package by its name and creates a symbolic link in a node_modules directory + */ +function createPackageLink(nodeModulesPath: string, packageName: string) { + // Resolve the packages path on disk + const destinationPath = path.dirname(require.resolve(packageName)); + const packageScope = packageName.includes('/') + ? packageName.split('/')[0] + : undefined; + + // Create a parent directory for a @scoped package + if (typeof packageScope === 'string') { + fs.mkdirSync(path.join(nodeModulesPath, packageScope)); + } + + const sourcePath = path.join(nodeModulesPath, packageName); + fs.symlinkSync(destinationPath, sourcePath); +} + +function createTempConfig(projectRoot: string, metroConfig: {...}) { + const content = ` + const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); + const config = ${JSON.stringify(metroConfig)}; + module.exports = mergeConfig(getDefaultConfig(__dirname), config); + `; + const configPath = path.join(projectRoot, 'metro.config.js'); + fs.writeFileSync(configPath, content, 'utf8'); + + const nodeModulesPath = path.join(projectRoot, 'node_modules'); + fs.mkdirSync(nodeModulesPath); + // Create a symbolic link to the '@react-native/metro-config' package used by the config + createPackageLink(nodeModulesPath, '@react-native/metro-config'); +} + +const configLoadingContext = { + reactNativePath: path.dirname(require.resolve('react-native/package.json')), + platforms: { + ios: {npmPackageName: 'temp-package'}, + android: {npmPackageName: 'temp-package'}, + }, +}; + +describe('loadMetroConfig', () => { + test('loads an empty config', async () => { + const rootPath = createTempPackage({name: 'temp-app'}); + createTempConfig(rootPath, {}); + + const loadedConfig = await loadMetroConfig({ + root: rootPath, + ...configLoadingContext, + }); + expect(loadedConfig.projectRoot).toEqual(rootPath); + expect(loadedConfig.watchFolders).toEqual([rootPath]); + }); + + test('loads watch folders', async () => { + const rootPath = createTempPackage({ + name: 'temp-app', + }); + createTempConfig(rootPath, { + watchFolders: ['somewhere-else'], + }); + + const loadedConfig = await loadMetroConfig({ + root: rootPath, + ...configLoadingContext, + }); + expect(loadedConfig.projectRoot).toEqual(rootPath); + expect(loadedConfig.watchFolders).toEqual([rootPath, 'somewhere-else']); + }); + + test('includes an npm workspace root if no watchFolders are defined', async () => { + const rootPath = createTempPackage({ + name: 'temp-root', + workspaces: ['packages/temp-app'], + }); + // Create a config inside a sub-package + const projectRoot = createTempPackage( + { + name: 'temp-app', + }, + path.join(rootPath, 'packages', 'temp-app'), + ); + createTempConfig(projectRoot, {}); + + const loadedConfig = await loadMetroConfig({ + root: projectRoot, + ...configLoadingContext, + }); + expect(loadedConfig.projectRoot).toEqual(projectRoot); + expect(loadedConfig.watchFolders).toEqual([projectRoot, rootPath]); + }); + + test('does not resolve an npm workspace root if watchFolders are defined', async () => { + const rootPath = createTempPackage({ + name: 'temp-root', + workspaces: ['packages/temp-app'], + }); + // Create a config inside a sub-package + const projectRoot = createTempPackage( + { + name: 'temp-app', + }, + path.join(rootPath, 'packages', 'temp-app'), + ); + createTempConfig(projectRoot, { + watchFolders: [], + }); + + const loadedConfig = await loadMetroConfig({ + root: projectRoot, + ...configLoadingContext, + }); + expect(loadedConfig.projectRoot).toEqual(projectRoot); + expect(loadedConfig.watchFolders).toEqual([projectRoot]); + }); +}); diff --git a/packages/community-cli-plugin/src/utils/__tests__/temporary-package.js b/packages/community-cli-plugin/src/utils/__tests__/temporary-package.js new file mode 100644 index 00000000000000..6f2ebf69c88446 --- /dev/null +++ b/packages/community-cli-plugin/src/utils/__tests__/temporary-package.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +export function createTempPackage( + packageJson: {...}, + packagePath: string = fs.mkdtempSync( + path.join(os.tmpdir(), 'rn-metro-config-test-'), + ), +): string { + fs.mkdirSync(packagePath, {recursive: true}); + if (typeof packageJson === 'object') { + fs.writeFileSync( + path.join(packagePath, 'package.json'), + JSON.stringify(packageJson), + 'utf8', + ); + } + + // Wrapping path in realpath to resolve any symlinks introduced by mkdtemp + return fs.realpathSync(packagePath); +} From 55fb330d3d6f69f9c49af90e1ec5dde9593e6b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 1 Feb 2024 23:34:00 +0100 Subject: [PATCH 18/20] Updated @react-native/metro-config return MetroConfig instead of ConfigT and remove default watchFolders --- packages/metro-config/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/metro-config/index.js b/packages/metro-config/index.js index 2abf2c8757d475..6ae5fd7c8c72df 100644 --- a/packages/metro-config/index.js +++ b/packages/metro-config/index.js @@ -8,7 +8,7 @@ * @noformat */ -/*:: import type {ConfigT} from 'metro-config'; */ +/*:: import type {InputConfigT} from 'metro-config'; */ const {getDefaultConfig: getBaseConfig, mergeConfig} = require('metro-config'); @@ -41,7 +41,7 @@ const INTERNAL_CALLSITES_REGEX = new RegExp( */ function getDefaultConfig( projectRoot /*: string */ -) /*: ConfigT */ { +) /*: InputConfigT */ { const config = { resolver: { resolverMainFields: ['react-native', 'browser', 'main'], @@ -82,14 +82,17 @@ function getDefaultConfig( }, }), }, - watchFolders: [], }; // Set global hook so that the CLI can detect when this config has been loaded global.__REACT_NATIVE_METRO_CONFIG_LOADED = true; + const defaults /* :InputConfigT */ = {...getBaseConfig.getDefaultValues(projectRoot)}; + // Deleting default empty watchFolders array allow a developer to explicitly specify it + delete defaults.watchFolders; + return mergeConfig( - getBaseConfig.getDefaultValues(projectRoot), + defaults, config, ); } From ab7816e761a9b7e4fba12f538fc2363c868e38e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 1 Feb 2024 23:34:50 +0100 Subject: [PATCH 19/20] Provide a default "undefined" override for watchFolders --- .../src/utils/loadMetroConfig.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/loadMetroConfig.js b/packages/community-cli-plugin/src/utils/loadMetroConfig.js index b233b873252c62..ed8e4ca33348fd 100644 --- a/packages/community-cli-plugin/src/utils/loadMetroConfig.js +++ b/packages/community-cli-plugin/src/utils/loadMetroConfig.js @@ -119,10 +119,16 @@ This warning will be removed in future (https://github.com/facebook/metro/issues } } - const config = await loadConfig({ - cwd, - ...options, - }); + const config = await loadConfig( + { + cwd, + ...options, + }, + { + // Enables users to explicitly specify watchFolders + watchFolders: undefined, + }, + ); const overrideConfig = getOverrideConfig(ctx, config); From 7fd9661b53972561cbf28311b5e3fe213756521b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Thu, 1 Feb 2024 23:37:08 +0100 Subject: [PATCH 20/20] Infer workspace root only if watchFolders are not explicitly specified --- .../src/utils/loadMetroConfig.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/community-cli-plugin/src/utils/loadMetroConfig.js b/packages/community-cli-plugin/src/utils/loadMetroConfig.js index ed8e4ca33348fd..8c471a1e0de7ff 100644 --- a/packages/community-cli-plugin/src/utils/loadMetroConfig.js +++ b/packages/community-cli-plugin/src/utils/loadMetroConfig.js @@ -54,6 +54,19 @@ function getOverrideConfig( ); } + // Always include the project root as a watch folder, since Metro expects this + const watchFolders = [config.projectRoot]; + + if (typeof config.watchFolders !== 'undefined') { + watchFolders.push(...config.watchFolders); + } else { + // Fallback to inferring a workspace root + const workspaceRoot = getWorkspaceRoot(ctx.root); + if (typeof workspaceRoot === 'string') { + watchFolders.push(workspaceRoot); + } + } + const overrides: InputConfigT = { resolver, serializer: { @@ -71,17 +84,9 @@ function getOverrideConfig( ), ], }, + watchFolders, }; - // Applying the heuristic of appending workspace root as watch folder, - // only if no other watch folder (beside the project root) has been given. - if (!config.watchFolders.some(folder => folder !== ctx.root)) { - const workspaceRoot = getWorkspaceRoot(ctx.root); - if (typeof workspaceRoot === 'string') { - overrides.watchFolders = [...config.watchFolders, workspaceRoot]; - } - } - return overrides; }