How to use symlinks in React Native project?
Asked Answered
C

7

9

The symlink support is still not officially available in react-native https://github.com/facebook/metro/issues/1.

It's actually possible to use symlinks in the package.json with npm (not yarn)

{
  "name": "PROJECT",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "test": "jest"
  },
  "dependencies": {
    "my_module1": "file:../shared/my_module1/",
    "my_module2": "file:../shared/my_module2/",
    "react": "16.8.3",
    "react-native": "0.59.5",
  },
  "devDependencies": {
    "babel-jest": "24.7.1",
    "jest": "24.7.1",
    "metro-react-native-babel-preset": "0.53.1",
    "react-test-renderer": "16.8.3"
},
"jest": {
    "preset": "react-native"
  }
}

Although we will get my_module1 does not exist in the Haste module map

To fix this we could do before a metro.config.js (formerly rn-cli.config.js)

const path = require("path")

const extraNodeModules = {
  /* to give access to react-native-firebase for a shared module for example */
  "react-native-firebase": path.resolve(__dirname, "node_modules/react-native-firebase"),
}
const watchFolders = [
  path.resolve(__dirname, "node_modules/my_module1"),
  path.resolve(__dirname, "node_modules/my_module2"),
]

module.exports = {
  resolver: {
    extraNodeModules
  },
  watchFolders,
  transformer: {
    getTransformOptions: async () => ({
      transform: {
        experimentalImportSupport: false,
        inlineRequires: false
      }
    })
  }
}

Unfortunately it doesn't work anymore on react-native 0.59 The app is reloading, but changes in the source code are not reflected in the app. Anyone has a clue to achieve this?

Calli answered 2/5, 2019 at 13:27 Comment(0)
C
2

In general

Don't forget to reset caches on every modifications on metro.config

yarn start --reset-cache

Add your local dependencies with are inside a folder link:../CompanyPackages/package e.g:

Package.json

"dependencies": {
  "local-package": "link:../CompanyPackages/local-package"
}

For react-native < 0.74

Use a custom metro.config.js

const path = require("path")
const { mapValues } = require("lodash")

// Add there all the Company packages useful to this app
const CompanyPackagesRelative = {
  "CompanyPackages": "../CompanyPackages",
}

const CompanyPackages = mapValues(CompanyPackagesRelative, (relativePath) =>
  path.resolve(relativePath)
)

function createMetroConfiguration(projectPath) {
  projectPath = path.resolve(projectPath)

  const watchFolders = [...Object.values(CompanyPackages)]
  const extraNodeModules = {
    ...CompanyPackages,
  }

  // Should fix error "Unable to resolve module @babel/runtime/helpers/interopRequireDefault"
  // See https://github.com/facebook/metro/issues/7#issuecomment-508129053
  // See https://dushyant37.medium.com/how-to-import-files-from-outside-of-root-directory-with-react-native-metro-bundler-18207a348427
  const extraNodeModulesProxy = new Proxy(extraNodeModules, {
    get: (target, name) => {
      if (target[name]) {
        return target[name]
      } else {
        return path.join(projectPath, `node_modules/${name}`)
      }
    },
  })

  return {
    projectRoot: projectPath,
    watchFolders,
    resolver: {
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
      extraNodeModules: extraNodeModulesProxy,
    },
  }
}

module.exports = createMetroConfiguration(__dirname)

For react-native => 0.74

const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config")

const path = require("path")
const { mapValues } = require("lodash")

// Add there all the Company packages useful to this app
const CompanyPackagesRelative = {
  "CompanyPackages": "../CompanyPackages",
}

const CompanyPackages = mapValues(CompanyPackagesRelative, (relativePath) =>
  path.resolve(relativePath)
)

function createMetroConfiguration(projectPath) {
  projectPath = path.resolve(projectPath)

  const watchFolders = [path.join(...Object.values(CompanyPackages))]

  const extraNodeModules = {
    ...CompanyPackages,
  }

  // Should fix error "Unable to resolve module @babel/runtime/helpers/interopRequireDefault"
  // See https://github.com/facebook/metro/issues/7#issuecomment-508129053
  // See https://dushyant37.medium.com/how-to-import-files-from-outside-of-root-directory-with-react-native-metro-bundler-18207a348427
  const extraNodeModulesProxy = new Proxy(extraNodeModules, {
    get: (target, name) => {
      if (target[name]) {
        return target[name]
      } else {
        return path.join(projectPath, `node_modules/${name}`)
      }
    },
  })

  return {
    watchFolders,
    resolver: {
      unstable_enableSymlinks: true,
      extraNodeModules: extraNodeModulesProxy,
    },
  }
}

const config = createMetroConfiguration(__dirname)

module.exports = mergeConfig(getDefaultConfig(__dirname), config)
Calli answered 4/11, 2022 at 11:26 Comment(1)
For people using Expo this is how you generate the metro file: npx expo customize metro.config.jsThirteen
I
4

You can use yalc. Yalc will simulate a package publish without actually publishing it.

Install it globally:

npm i -g yalc

In your local package:

yalc publish

In the application:

yalc add package-name && yarn

After you made some changes to the package, you can just run

yalc push

and it will automatically update every app that uses your local package

Incorporation answered 3/1, 2022 at 21:25 Comment(2)
still works 11/2023. this is the right answerLuisluisa
sad that I can only upvote this answer once :(Pearlypearman
C
3

I had a similar issue and found haul.

  1. Follow haul getting started instructions.
  2. Add your local dependencies with file:../ e.g:
// package.json
"dependencies": {
  "name-of-your-local-dependency": "file:../"
}
  1. reinstall node_modules with yarn --force
  2. Start the development server by yarn start (haul will replace your start script)
  3. Run react-native run-android or/and react-native run-ios
Calamanco answered 31/7, 2019 at 20:51 Comment(0)
W
2

None of the answers I found worked for me and it seems symlinks are not going to be supported anytime soon (see: https://github.com/facebook/metro/issues/1), so I had to do it manually.

I am using onchange npm package and running that in my local package: onchange 'dist/**/*.js' -- cp -R ./dist ../../app/node_modules/@author/packagename

That worked for me to achieve development and testing, while not breaking anything for production releases. I hope this can save my peers a few headaches.

Wilk answered 17/2, 2021 at 16:15 Comment(0)
C
2

In general

Don't forget to reset caches on every modifications on metro.config

yarn start --reset-cache

Add your local dependencies with are inside a folder link:../CompanyPackages/package e.g:

Package.json

"dependencies": {
  "local-package": "link:../CompanyPackages/local-package"
}

For react-native < 0.74

Use a custom metro.config.js

const path = require("path")
const { mapValues } = require("lodash")

// Add there all the Company packages useful to this app
const CompanyPackagesRelative = {
  "CompanyPackages": "../CompanyPackages",
}

const CompanyPackages = mapValues(CompanyPackagesRelative, (relativePath) =>
  path.resolve(relativePath)
)

function createMetroConfiguration(projectPath) {
  projectPath = path.resolve(projectPath)

  const watchFolders = [...Object.values(CompanyPackages)]
  const extraNodeModules = {
    ...CompanyPackages,
  }

  // Should fix error "Unable to resolve module @babel/runtime/helpers/interopRequireDefault"
  // See https://github.com/facebook/metro/issues/7#issuecomment-508129053
  // See https://dushyant37.medium.com/how-to-import-files-from-outside-of-root-directory-with-react-native-metro-bundler-18207a348427
  const extraNodeModulesProxy = new Proxy(extraNodeModules, {
    get: (target, name) => {
      if (target[name]) {
        return target[name]
      } else {
        return path.join(projectPath, `node_modules/${name}`)
      }
    },
  })

  return {
    projectRoot: projectPath,
    watchFolders,
    resolver: {
      transform: {
        experimentalImportSupport: false,
        inlineRequires: true,
      },
      extraNodeModules: extraNodeModulesProxy,
    },
  }
}

module.exports = createMetroConfiguration(__dirname)

For react-native => 0.74

const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config")

const path = require("path")
const { mapValues } = require("lodash")

// Add there all the Company packages useful to this app
const CompanyPackagesRelative = {
  "CompanyPackages": "../CompanyPackages",
}

const CompanyPackages = mapValues(CompanyPackagesRelative, (relativePath) =>
  path.resolve(relativePath)
)

function createMetroConfiguration(projectPath) {
  projectPath = path.resolve(projectPath)

  const watchFolders = [path.join(...Object.values(CompanyPackages))]

  const extraNodeModules = {
    ...CompanyPackages,
  }

  // Should fix error "Unable to resolve module @babel/runtime/helpers/interopRequireDefault"
  // See https://github.com/facebook/metro/issues/7#issuecomment-508129053
  // See https://dushyant37.medium.com/how-to-import-files-from-outside-of-root-directory-with-react-native-metro-bundler-18207a348427
  const extraNodeModulesProxy = new Proxy(extraNodeModules, {
    get: (target, name) => {
      if (target[name]) {
        return target[name]
      } else {
        return path.join(projectPath, `node_modules/${name}`)
      }
    },
  })

  return {
    watchFolders,
    resolver: {
      unstable_enableSymlinks: true,
      extraNodeModules: extraNodeModulesProxy,
    },
  }
}

const config = createMetroConfiguration(__dirname)

module.exports = mergeConfig(getDefaultConfig(__dirname), config)
Calli answered 4/11, 2022 at 11:26 Comment(1)
For people using Expo this is how you generate the metro file: npx expo customize metro.config.jsThirteen
M
0

Could never get my own environment working using any other suggestions, but found a hack that works well (though not ideal) that can be easily set up in just a few lines of code and without changing your RN project configuration.

Use fs.watch for changes recursively in the directory where you're working on your library, and copy the updates over whenever there's been a change:

import fs from 'fs'

const srcDir = `./your-library-directory`
const destDir = `../your-destination-directory`

fs.watch("./src/", {recursive: true}, () => {
  console.log('copying...')
  fs.cp(srcDir, destDir, { overwrite: true, recursive: true }, function() {
    console.log('copied')
  })
})
Miltiades answered 4/12, 2022 at 18:22 Comment(0)
S
0

Only two things are required. 1.yarn --force 2.yarn start

then select android for running option

Serajevo answered 7/2, 2023 at 3:32 Comment(0)
F
0

You can enable the symbolic link by updating the package.json and metro.config.js

// package.json

"dependencies": {
"react": "18.2.0",
"react-native": "0.74.2",
"my-button": "file:src/custom-button"}

// metro.config.js

const config = {
watchFolders: [
    'src/custom-button',
],

resolver: {
    unstable_enableSymlinks: true}};
Forestforestage answered 4/7 at 5:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.