react-native-monorepo-config 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +76 -0
  3. package/index.js +113 -0
  4. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Krister Kari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # react-native-monorepo-config
2
+
3
+ Helper to configure Metro for a React Native app in a monorepo.
4
+
5
+ ## Why
6
+
7
+ [Metro](https://metrobundler.dev/) ([React Native](https://reactnative.dev)'s bundler) doesn't work well with monorepos out of the box. This package is intended to make the configuration easier.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ yarn add -D react-native-monorepo-config
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ Let's consider the following monorepo structure:
18
+
19
+ ```sh
20
+ my-monorepo
21
+ ├── apps
22
+ │ └── my-app
23
+ └── packages
24
+ ├── a/
25
+ └── b/
26
+ ```
27
+
28
+ Here, `my-app` is a React Native app, and `a` and `b` are libraries that are used in the app.
29
+
30
+ To configure Metro for `my-app`, you can create a `metro.config.js` file in the `my-app` directory with the following content:
31
+
32
+ ```js
33
+ const { getDefaultConfig } = require("@react-native/metro-config"); // Import from `@expo/metro-config` if using Expo CLI
34
+ const { withMetroConfig } = require("react-native-monorepo-config");
35
+
36
+ module.exports = withMetroConfig(
37
+ getDefaultConfig(__dirname), // Metro config to extend
38
+ {
39
+ root: path.resolve(__dirname, "../.."), // Path to the monorepo root
40
+ dirname: __dirname, // Path to the current directory
41
+ }
42
+ );
43
+ ```
44
+
45
+ It's also recommended to disable hoisting for the React Native app's dependencies to avoid issues.
46
+
47
+ For Yarn 4, hoisting can be disabled by setting `nmHoistingLimits: 'workspaces'` in `.yarnrc.yml`. See [Yarn documentation](https://yarnpkg.com/configuration/yarnrc#nmHoistingLimits) for more details.
48
+
49
+ If you want to customize the returned Metro config, remember to merge the returned config with your custom config. For example:
50
+
51
+ ```js
52
+ const monoRepoConfig = withMetroConfig(getDefaultConfig(__dirname), {
53
+ root: path.resolve(__dirname, "../.."),
54
+ dirname: __dirname,
55
+ });
56
+
57
+ module.exports = {
58
+ ...monoRepoConfig,
59
+
60
+ resolver: {
61
+ ...monoRepoConfig.resolver,
62
+
63
+ assetExts: resolver.assetExts.filter((ext) => ext !== "svg"),
64
+ sourceExts: [...resolver.sourceExts, "svg"],
65
+ },
66
+ };
67
+ ```
68
+
69
+ ## How it works
70
+
71
+ This configuration will setup a few things:
72
+
73
+ - Configure Metro to watch for changes in other packages in the monorepo instead of just the current package. This may slow down the bundling process, in large monorepos. In that case, you can override `watchFolders` to add specific folders to watch instead.
74
+ - Block packages defined in `peerDependencies` of other packages in the monorepo to avoid duplicate versions from being loaded. Loading duplicate versions of some packages such as `react` can cause issues. Make sure to specify `peerDependencies` for your packages appropriately.
75
+ - If the packages defined in `peerDependencies` have been hoisted to the monorepo root, point Metro to them so they can be found.
76
+ - Configure Metro's resolve to prioritize `package.json#source` or the `source` condition in `package.json#exports` so that the app can import source code directly from other packages in the monorepo. To utilize this, make sure to add `"source": "src/index.ts"` or `"exports": { ".": { "source": "./src/index.ts" } }` to the `package.json` of the packages you want to import from.
package/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import escape from 'escape-string-regexp';
2
+ import glob from 'fast-glob';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ /**
7
+ * Configure Metro for a React Native app in a monorepo.
8
+ *
9
+ * @param {import('metro-config').MetroConfig} baseConfig Base Metro config to extend.
10
+ * @param {string} options.root Root directory path of the monorepo.
11
+ * @param {string} options.dirname Directory path of the current package.
12
+ *
13
+ * @returns {import('metro-config').MetroConfig}
14
+ */
15
+ export function withMetroConfig(baseConfig, { root, dirname }) {
16
+ const pkg = JSON.parse(
17
+ fs.readFileSync(path.join(root, 'package.json'), 'utf8')
18
+ );
19
+
20
+ if (pkg.workspaces == null) {
21
+ throw new Error(
22
+ `No 'workspaces' field found in the 'package.json' at '${root}'.`
23
+ );
24
+ }
25
+
26
+ // Get the list of monorepo packages except current package
27
+ const packages = (pkg.workspaces.packages || pkg.workspaces)
28
+ .flatMap((pattern) =>
29
+ glob.sync(pattern, {
30
+ cwd: root,
31
+ onlyDirectories: true,
32
+ ignore: [`**/node_modules`, `**/.git`, `**/.yarn`],
33
+ })
34
+ )
35
+ .filter((p) => {
36
+ const dir = path.join(root, p);
37
+
38
+ // Ignore current package
39
+ if (path.relative(dir, dirname) === '') {
40
+ return false;
41
+ }
42
+
43
+ // Ignore packages that don't have a package.json
44
+ return fs.existsSync(path.join(dir, 'package.json'));
45
+ });
46
+
47
+ // Get the list of peer dependencies for all packages in the monorepo
48
+ const peers = packages
49
+ .flatMap((it) => {
50
+ const pak = JSON.parse(
51
+ fs.readFileSync(path.join(root, it, 'package.json'), 'utf8')
52
+ );
53
+
54
+ return pak.peerDependencies ? Object.keys(pak.peerDependencies) : [];
55
+ })
56
+ .sort()
57
+ .filter(
58
+ (m, i, self) => self.lastIndexOf(m) === i // Remove duplicates
59
+ );
60
+
61
+ // We need to exclude the peerDependencies we've collected in packages' node_modules
62
+ // Otherwise duplicate versions of the same package will be loaded
63
+ const blockList = new RegExp(
64
+ '(' +
65
+ packages
66
+ .flatMap((it) =>
67
+ peers.map((m) => `^${escape(path.join(it, 'node_modules', m))}\\/.*$`)
68
+ )
69
+ .join('|') +
70
+ ')$'
71
+ );
72
+
73
+ // When we import a package from the monorepo, metro won't be able to find their deps if they are hoisted
74
+ // We need to specify them in `extraNodeModules` to tell metro where to find them
75
+ const extraNodeModules = peers.reduce((acc, name) => {
76
+ if (fs.existsSync(path.join(root, 'node_modules', name))) {
77
+ acc[name] = path.join(root, 'node_modules', name);
78
+ }
79
+
80
+ return acc;
81
+ }, {});
82
+
83
+ /** @type {import('metro-config').MetroConfig} */
84
+ return {
85
+ ...baseConfig,
86
+
87
+ projectRoot: dirname,
88
+
89
+ // We need to watch the root of the monorepo
90
+ // This lets Metro find the monorepo packages automatically using haste
91
+ // This also lets us import modules from monorepo root
92
+ watchFolders: [root],
93
+
94
+ resolver: {
95
+ ...baseConfig.resolver,
96
+
97
+ blockList,
98
+ extraNodeModules,
99
+ resolveRequest: (context, realModuleName, platform) => {
100
+ // Prefer the source field for monorepo packages to consume source code
101
+ if (packages.includes(realModuleName)) {
102
+ context.mainFields = ['source', ...context.mainFields];
103
+ context.unstable_conditionNames = [
104
+ 'source',
105
+ ...context.unstable_conditionNames,
106
+ ];
107
+ }
108
+
109
+ return context.resolveRequest(context, realModuleName, platform);
110
+ },
111
+ },
112
+ };
113
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "react-native-monorepo-config",
3
+ "version": "0.1.0",
4
+ "description": "Configure Metro for a React Native app in a monorepo",
5
+ "repository": "https://github.com/satya164/react-native-monorepo-config",
6
+ "author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/)",
7
+ "license": "MIT",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "type": "module",
13
+ "main": "index.js",
14
+ "files": [
15
+ "index.js"
16
+ ],
17
+ "dependencies": {
18
+ "escape-string-regexp": "^5.0.0",
19
+ "fast-glob": "^3.3.3"
20
+ },
21
+ "devDependencies": {
22
+ "@release-it/conventional-changelog": "^10.0.1",
23
+ "prettier": "^3.5.3",
24
+ "release-it": "^19.0.1"
25
+ },
26
+ "prettier": {
27
+ "quoteProps": "consistent",
28
+ "tabWidth": 2,
29
+ "useTabs": false,
30
+ "singleQuote": true,
31
+ "trailingComma": "es5"
32
+ },
33
+ "release-it": {
34
+ "git": {
35
+ "commitMessage": "chore: release ${version}",
36
+ "tagName": "v${version}"
37
+ },
38
+ "npm": {
39
+ "publish": true
40
+ },
41
+ "github": {
42
+ "release": true
43
+ },
44
+ "plugins": {
45
+ "@release-it/conventional-changelog": {
46
+ "preset": {
47
+ "name": "conventionalcommits"
48
+ },
49
+ "infile": "CHANGELOG.md"
50
+ }
51
+ }
52
+ }
53
+ }