DEV Community

Cover image for Setting up a multi-package project
André Vital
André Vital

Posted on • Edited on • Originally published at andrevital.com

Setting up a multi-package project

This is a simple project setup guide for npm-based multi-package projects. It takes advantage of yarn workspaces for the multi-package handling part (we'll be using yarn as package manager).

For this example, we'll setup everything for two sub-packages (frontend & backend). Create your packages accordingly, following this basic folder structure:

my-project
└── packages
    ├── backend
    │   └── package.json
    └── frontend
        └── package.json
Enter fullscreen mode Exit fullscreen mode

Assuming we have already created both base sub-packages, from the project root folder run the following command:

rm -rf packages/**/node_modules/ packages/**/package-lock.json packages/**/yarn.lock
Enter fullscreen mode Exit fullscreen mode

This will get rid of all previously installed dependencies per sub-package and existing lock files.

Afterwards, we'll initialize our project by running the following command from the project root folder:

yarn init -yp
Enter fullscreen mode Exit fullscreen mode

This will create a package.json file with default values provided by Yarn, with "private": true added.

We'll do some basic changes to the package.json file we're left with:

// my-project/package.json
{
  // We can get rid of "main"
  "name": "my-project", // The name of your project. Optionally removable
  "version": "1.0.0", // Or any version you desire. Optionally removable
  "private": true,  // By having the project be private, we enable yarn workspaces
  "scripts": {
    "build": "yarn workspaces run build",
    // Commands to run FE & BE directly from project root
    "dev:frontend": "yarn workspace @my-project/frontend start:dev",
    "dev:backend": "yarn workspace @my-project/backend start:dev",
  },
  "engines": {
    "node": "^16.16.0"
  },
  // Our base packages (workspaces) folder
  "workspaces": {
    "packages": [
      "packages/*"
    ]
  },
  "license": "MIT" // Or any license you'll be using for your project. Optionally removable
}
Enter fullscreen mode Exit fullscreen mode

Once these changes are completed, we'll also need to update the respective package.json files for all sub-packages with the following:

// my-project/packages/{package-name}/package.json
{
  "name": "@my-project/{package-name}", // e.g. "name": "@my-project/frontend",
  "private": true, // Ensure this line exists to enable yarn workspaces
  "scripts": {
    // ...
    "start:dev": "Command to start development", // e.g. "start:dev": "next dev",
    "build": "Command to run build", // e.g. "build": "next build",
    // ...
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

We'll also need to add the respective .gitignore files to prevent undesired file tracking. Here's a basic example for the project root:

# node
node_modules/

# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.log/

# dotenv
.env

# local editors
.vscode
.DS_Store
Enter fullscreen mode Exit fullscreen mode

Finally, we run the yarn command from the root folder, and voilá!, all sub-package dependencies are now installed.

We can now run yarn dev:{package-name} from our project root folder to start the desired sub-package development server or yarn build to build all sub-packages.

Formatting & linting (Optional)

This section will contain additional project configuration focused towards project linting & formatting. If you desire to proceed without this, feel free to skip to the next section.

We'll use ESLint with some plugins, alongside Prettier for project linting & file formatting.

We'll also add Husky and git-format-staged (a sweet dependency by Jesse Hallett) for even nicer file formatting configuration.

Run the following command to install all additional dependencies:

yarn add -DW \
  cross-env \
  eslint \
  eslint-config-prettier \
  eslint-plugin-flowtype \
  eslint-plugin-import \
  eslint-plugin-prettier \
  eslint-plugin-promise \
  git-format-staged \
  husky \
  prettier
Enter fullscreen mode Exit fullscreen mode

After the installation is completed, we'll create a custom yarn script to add a fix for Husky hooks in sub-packages. Do so with the following command:

mkdir scripts && touch scripts/yarn-prepare.sh
Enter fullscreen mode Exit fullscreen mode

This sould result in the following folder structure:

my-project
├── node_modules
├── package.json
├── packages
│   ├── backend
│   │   └── package.json
│   └── frontend
│       └── package.json
├── scripts
│   └── yarn-prepare.sh
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

Add the following to the contents of the script file:

# yarn-prepare.sh
#! /bin/bash

set -e

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

HUSKY_LOCAL_SH="$DIR/../.git/hooks/husky.local.sh"
if [[ -f "$HUSKY_LOCAL_SH" ]]; then
  sed -i'' -E 's/cd \".+\"/cd \".\"/' "$HUSKY_LOCAL_SH";
fi
Enter fullscreen mode Exit fullscreen mode

Next, we'll add three extra commands to our root package.json

// my-project/package.json
{
  // ...
  "scripts": {
    // ...
    "prepare": "cross-env-shell scripts/yarn-prepare.sh",
    "lint": "yarn workspaces run lint",
    "fix": "yarn workspaces run lint --fix"
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

We'll also need to add the corresponding commands for each sub-package:

// my-project/packages/{package-name}/package.json
{
  // ...
  "scripts": {
    // ...
    "lint": "eslint --cache {your-file-selection}",
    // Examples:
    //  "lint": "eslint --cache --ext .ts,.tsx .",
    //  "lint": "eslint --cache \"{src,apps,libs,test}/**/*.{ts,tsx}\"",
    "prepare": "cross-env-shell ../../scripts/yarn-prepare.sh"
    // ...
  },
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Once all changes are put in, we can proceed with our Husky hook. We'll create a file called .huskyrc.json at our project root and add the following contents to it:

// my-project/.huskyrc.json
{
  "hooks": {
    "pre-commit": "git-format-staged -f 'prettier --stdin-filepath \"{}\"' {your-file-extensions}"
    // e.g. "pre-commit": "git-format-staged -f 'prettier --stdin-filepath \"{}\"' '*.ts' '*.tsx' '*.json' '*.md' '*.markdown'"
  }
}
Enter fullscreen mode Exit fullscreen mode

This will ensure all selected file extensions get properly formatted.

After that, we create our Prettier configuration file. We'll call it .prettierrc and also place it at our project root folder. You can add any custom configuration you desire from the Prettier Configuration Options. Here's an example provided:

// my-project/.prettierrc
{
  "arrowParens": "always",
  "semi": false,
  "trailingComma": "all",
  "useTabs": true,
  "tabWidth": 4,
  "singleQuote": false,
  "bracketSpacing": true
}
Enter fullscreen mode Exit fullscreen mode

Finally, with the addition of ESLint, when linting, we'll craete .eslintcache files. We'll want to add those to our .gitignore files to avoid tracking them:

# my-project/.gitignore 

# eslint
.eslintcache
Enter fullscreen mode Exit fullscreen mode

Finally, we'll want to add the corresponding .eslintrc.js files, with the desired rules for each case. You can get a better ideaa on how to achieve this by taking a look at ESLint's Configuration Guide. We can even create a base .eslintrc.base.js and place it inside our packages and have sub-packages' configuration files extend from it:

// my-projects/packages/.eslintrc.base.js
module.exports = {
  extends: [
    "plugin:prettier/recommended",
    "plugin:flowtype/recommended",
    "plugin:import/recommended",
    "plugin:promise/recommended",
  ],
}
Enter fullscreen mode Exit fullscreen mode

Here's an example configuration file for a Next.js application with TypeScript

// my-project/packages/frontend/.eslintrc.js
module.exports = {
  extends: "../.eslintrc.base.js", // Here's how we extend from our base configuration file
  parserOptions: {
    tsconfigRootDir: __dirname,
    project: "./tsconfig.json",
  },
  env: {
    node: true,
  },
  rules: {
    "@typescript-eslint/no-unused-vars": "off",
    "react/react-in-jsx-scope": "off",
    "jsx-a11y/anchor-is-valid": "off",
    "@next/next/no-img-element": "off",
    "@next/next/no-css-tags": "off",
  },
  overrides: [
    {
      files: ["*.tsx"],
      rules: {
        "@typescript-eslint/explicit-function-return-type": ["off"],
        "@typescript-eslint/explicit-module-boundary-types": ["off"],
      },
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

And that's it! We now have a fully functional multi-packages project configured! (maybe even formatting/linting enabled, if you didn't skip that part)

You can take a look at an example repository containing all that was done in this GitHub repo, and the version with the formatting/linting changes on this branch.

Top comments (0)