Local Cross-Project Component Development with Bit Link Target

be
benjyg8 months ago

You're managing your component library - creating new components, updating existing components. And you want to test the updates in an application that consumes those components, in a separate repo on your machine.

You could tag and export after every change, and then update the dependency version in your consuming application. But that's not a dev ex that can scale.

What you'd ideally like to do is somehow locally transfer the changes to your consuming project, and have those changes automatically update in the consuming application.

Thankfully Bit can help 😎

For those who prefer following video tutorials, here is this post's sister video:

Solution

Bit's link-target feature - in 2 quick steps:

(Step 0: Run bit watch in your component workspace - this ensures you dont need to manually compile components after each change)

Step 1: Fetch the path of the target project root directory (e.g. by running pwd in the consuming project)

Step 2: run the following command in the component workspace (replace path with your target project's path):

$bit
Copiedcopy
See command synopsis
Clear Bundler Cache

At this point you will likely need to clear the consuming application's bundler cache.
For webpack that's in the node_modules/.cache directory.
For vite it's node_modules/.vite.
Delete the relevant directory and restart the application's dev server and you should now start seeing your component updates.

And that's it!

It's really that simple - now any changes you make in your component workspace will be immediately reflected in your consuming application.

In fact you can use this feature to link a component workspace to multiple consuming projects simultaneously!

Verify Symlinks

You can verify, even before running the application, that your consuming app is linked to the component workspace by hovering over the path in the import statement in your application code. The IDE should show you the path to the imported module, which should now show the path to the component workspace, not the version in local node_modules.

Revert the Consuming Project (removing symlinks)

To restore your application to the regular installed versions of the components, simply re-run dependency installation in your application (e.g. npm install) - the symlinks will be overwritten and you'll be back to the project state before bit link --target.

If you've set up a postinstall script as described below, make sure to remove that from your package.json too.

Background and troubleshooting

When you run bit link target Bit creates symlinks from the component workspace's node_modules directory to the consuming project's node_modules, for all the components in the workspace.

Under the hood it uses npm link, but in a targeted way in order to specifically link folders relevant to the components in the workspace and not other, unnecessary packages.

When you change any component's code (and have bit watch running in the background, or run bit compile per change) those updates are compiled by Bit to the component's node_modules directory in the component workspace, which has now been symlinked to your application project. So now your application project is 'looking' at the live, updated version of your component/s.

The --peers flag

This is where the link --target feature really comes into its own.

Symlinking between projects isn't as simple as it sounds - for symlinking components to work you can't only link the components to the target project, you must also link any peerDependencies of the linked components too (we won't go into the intricacies of that here, but suffice to say that your application won't function as expected in numerous scenarios if peerDependencies are not linked too).

Bit automatically calculates dependency graphs for all components as part of its feature set, so Bit knows which packages need to be symlinked as peerDependencies to complete the link process to the consuming application. The --peers flag tells Bit to link those peer dependencies too, and ensures the feature will work in any scenario.

tip

A previous version of this feature, from mid-2023, did not have the --peers flag and had 'blind spots', which have now been filled - so if you've tried this before and it didn't work for you, it's worth trying again now.

Hot Reload

Depending on your bundler and its config, your dev server may not be set up to monitor changes in node_modules.

For webpack you can add the following to your application's webpack.config.js - replace @org with your Bit org:

watchOptions: { 
        ignored: [
            'node_modules/(?!@org/.+)',
            '@org/.+/node_modules',
      ],
    },
CopiedCopy

For vite you can add the following to your application's vite.config.js:

server: {
    watch: {
      ignored: ['!**/node_modules/@your-org/**']
    }
  },
  // The watched package must be excluded from optimization,
  // so that it can appear in the dependency graph and trigger hot reload.
  optimizeDeps: {
    exclude: ['@your-org']
  }
CopiedCopy

When To Re-link

As noted above it's easy to revert the project back to its non-linked state, but that also means this could happen inadvertantly by installing packages in the consuming application while you're still using the symlinks. So if your component updates suddenly stop updating in the consuming application, this is probably what's happened. Just run bit link --target again to re-establish the symlinks as before.

Automated Re-linking

You can create a node script to run in the consuming application/s package.json to re-sync the symlinks after running npm install:

'use strict';

// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';

const { exec } = require('child_process');
const path = require('path');

// Define the directory where you want to run the command
const componentsWorkspaceRoot = '/Users/benjamingilbert/path/to/components-workspace';
const thisDirectory = path.resolve('.');
console.log(thisDirectory);

// Define the command you want to run
const commandToRun = `bit link --target ${thisDirectory} --peers`;

// Run the command in the specified directory
exec(commandToRun, { cwd: componentsWorkspaceRoot }, (error, stdout, stderr) => {
  if (error) {
    console.error(`Error executing command: ${error}`);
    return;
  }
  
  if (stderr) console.error(`Command error: ${stderr}`) 
  else console.log(`Command output: ${stdout}`);
});
CopiedCopy

You can either run this script manually from your consuming application, e.g. in this case via npm run sync-bit:

// package.json
{
  "scripts": {
    "sync-bit": "node ./scripts/link-components.js"
  }
}
CopiedCopy

Or if want this to be fully automated, add it to node's built-in postinstall script, which will run it automatically after any npm install::

{
  "scripts": {
    "postinstall": "node ./scripts/link-components.js"
  }
}
CopiedCopy
Dont forget to revert

Make sure to remove this postinstall script from your package.json when you want to revert your project back to its unlinked state