Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using next.js with yarn workspaces

Ran into a complication today with project structure like this

packages
  /app
    pages/
    package.json
  /ui-kit
    pages/
    package.json
  /shared
.babelrc
package.json

root lvl package json defines workspaces: [packages/*] where app and ui-kit are both nextjs apps.

I have following script in root lvl package.json

"dev:app": "next packages/app",
"dev:ui-kit": "next packages/ui-kit"

both of these worked fine until I introduced shared folder, which essentially contains some functions / components etc... that are re-used between packages. As soon as I include it into either app or ui-kit I get error like this

in ./packages/shared/index.js

Module parse failed: Unexpected token (4:21) You may need an appropriate loader to handle this file type. | import React from 'react' | | export default () => Hello shared! |

So it looks like nextjs is not applying any loaders to anything outside the folder where it was pointed at. Is there a solution to fix this somehow? i.e. start next from root folder but point it to different entry files somehow based on different script commands?

like image 268
Ilja Avatar asked Jul 02 '18 14:07

Ilja


People also ask

Can I use yarn workspaces with NPM?

The package. json in the root has a new “workspaces” section which tells yarn or npm where the packages are. With this in place, you only need to run npm install or yarn install once, in the root of the directory.

How do yarn workspaces work?

Yarn Workspaces is a feature that allows users to install dependencies from multiple package. json files in subfolders of a single root package. json file, all in one go. Yarn can also create symlinks between Workspaces that depend on each other, and will ensure the consistency and correctness of all directories.

What is Workspace aggregator yarn?

Workspaces are a new way to set up your package architecture that's available by default starting from Yarn 1.0. It allows you to setup multiple packages in such a way that you only need to run yarn install once to install all of them in a single pass.


1 Answers

Since NextJs 11, there's a new experimental option called externalDir which works pretty well and does not require the use of next-transpile-modules.

For clarity let's make a step by step howto, it might look a long process but once you get it, it's pretty easy (actually 3 steps)


1. Yarn V3 (optional)

For improved experience I suggest to upgrade yarn to v3+ (yarn set version 3.0.2 && yarn plugin import workspace-tools) and edit the generated config .yarnrc.yml similar to this one:

# Yarn 2+ supports pnp or regular node_modules installs. Use node-modules one.
nodeLinker: node-modules
nmMode: hardlinks-local
plugins:
  - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
    spec: "@yarnpkg/plugin-workspace-tools"
yarnPath: .yarn/releases/yarn-3.0.2.cjs

PS: you might want to add this to .gitignore too

.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*

Why ? Cause you'll get the possibility to use the workspace: alias protocol. (available in pnpm too)


2. Strict workspace topology (optional)

I suggest to be strict about what packages depends on (to have clear boundaries). It's not an absolute requirement, but a good practice that might avoid hard to debug situations.

To help the package manager, I suggest to properly declare your dependencies and their boundaries per app/packages.

In other words each package/app had its own package.json where you explicitly add the deps they need (not in the root package.json)

Following your example,

apps/
packages
  /app
    package.json (app depend on ui-kit through yarn workspace: alias)
    tsconfig.json (we will add typescript path aliases there too) 
    next.config.js
  /ui-kit
    package.json
package.json (do not put nextjs as dep here, only in app)

An example for root package.json

{
  "name": "monorepo",
  "private": true,
  "workspaces": [
    "packages/*"  // Enable package discovery in packages/* directory.
  ],
  "devDependencies": {
    "husky": "7.0.2", // Only what's needed for monorepo management
  }

An example for packages/app/package.json

{
  "name": "my-app",
  "devDependencies": {
    "@types/node": "16.10.1",
    "@types/react": "17.0.29",
    "@types/react-dom": "17.0.9",
    "typescript": "4.4.4"
  },
  "dependencies": {
    // Assuming the name of packages/ui-kit is ui-kit,
    // we explicitly declare the dependency on it through
    // workspace: alias (package-manager perspective)
    "ui-kit": "workspace:*",
    "next": "11.1.2",
    "react": "17.0.2",
    "react-dom": "17.0.2",
  }
}

Why ? That way you won't fall into weird problems with conflicting deps.


3. Typescript aliases

Even if you're not using typescript, NextJs will read the tsconfig.json and look for typescript path mapping configuration. If you're not aware of what it is... it's simply a configuration where you declare (one more time) your deps. Nextjs will convert them to what it uses under the hood to compile deps (ie: babel-plugin-module-resolver and probably later swc).

Following your example, just edit a ./packages/app/tsconfig.json in this way

{
  "compilerOptions": {
    // here baseUrl is set at ./src (good practive), can
    // be set to '.'  
    "baseUrl": "./src",
    "paths": {
      // Declare deps here (keep them in sync with what
      // you defined in the package.json)
      // PS: path are relative to baseUrl
      "ui-kit/*": ["../../ui-kit/src/*"],
      // if you have a barrel in ui-lib 
      "ui-kit": ["../../ui-kit/src/index"],
    }
  },
}

Why ? More a limitation between tooling (package managers and paths have different perspectives)


4. Nextjs configuration

In packages/app/nextjs.config.js, enable the externalDir config (currently in experimental but works pretty well, feedback thread here)

const nextConfig = {
  experimental: {
    // this will allow nextjs to resolve files (js, ts, css)
    // outside packages/app directory. 
    externalDir: true,
  },
};
export default nextConfig;

PS: for older nextjs versions, it's totally possible to do the same through custom webpack config. Ask if you need an example.


What you'll get

In your app you should be able to import your ui-kit like this:

import { Button } from 'ui-kit';
// or
import Avatar from 'ui-kit/components/Avatar'

The beauty of it is that fast refresh will work out of the box (no build necessary). It's fast, you don't need NX (+ the pricey nx.cloud), rush or anything...

Nextjs will simply import the files, build them on demand and even cache them in it's own optimized cache (especially fast with webpack 5 and can be enabled on CI too)...

If you like more information, I maintain an example repository will a full lifecycle perspective (ci, github action, linters, deploys...) on this repo: https://github.com/belgattitude/nextjs-monorepo-example.

PS: Follow also yarn 3+ development and versions here, they're doing a great job nowadays.

like image 59
Sébastien Vanvelthem Avatar answered Sep 17 '22 04:09

Sébastien Vanvelthem