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?
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.
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.
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.
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)
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)
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.
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)
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With