TypeScript "paths" in monorepo #620
-
Let's say I want to use typescript's "paths" to simplify my imports. I'll use the current starter project as an example, with the idea to use path aliases in the Here's the CodeSandbox showing the issue. In the
Then I could import button like this:
However, in the apps that consume this
Any ideas? One solution: build
|
Beta Was this translation helpful? Give feedback.
Replies: 20 comments 39 replies
-
To eliminate the build step, you need add the {
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~*": ["../../packages/ui/src/*"]
}
}
} Next.js is smart enough to pick up the Also make sure you have the You can also use the same path alias for the |
Beta Was this translation helpful? Give feedback.
-
For anyone working on CSR apps and use bundlers like webpack, vite, rollup, etc. make sure to configure them to understand the aliases. Important note on vite and rollup. They do not accept an array of paths for each alias, so we are forced to use a unique alias for each path. Namely, if I use resolve: {
alias: [
{
find: '@',
replacement: [path.resolve(__dirname, 'src'), path.resolve(__dirname, '../../packages/ui/src')], // replacement property expect a string, not array
},
],
},
// OR alias set as object
resolve: {
alias: {
'@': [path.resolve(__dirname, 'src'), path.resolve(__dirname, '../../packages/ui/src')], // Error: Type 'string[]' is not assignable to type 'string'.
},
}, The only way to make this work is to have different internal aliases for each project. For example, I used // packages/ui/tsconfig.json
{
"extends": "@config/tsconfig.react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@ui/*": ["./src/*"] // no other project in the monorepo uses this alias
}
},
"include": ["."]
}
// apps/web/tsconfig.json
{
"extends": "@config/tsconfig.react.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@ui/*": ["../../packages/ui/src/*"]
}
},
"exclude": ["node_modules", "public"],
"include": ["."]
}
// apps/web/vite.config.ts
// ... left out irrelevant part of the vite config
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@ui': path.resolve(__dirname, '../../packages/ui/src'),
},
},
// apps/web/src/App.tsx
import { Button } from '@ui'; // import from ui package
import { AppLayout } from '@/layouts'; // internal import using alias for apps/web/src/layouts path
export function App() {
return (
<AppLayout>
<h1>My Web App</h1>
<Button onClick={() => alert('You clicked me!')}>Click Me</Button>
</AppLayout>
)
} |
Beta Was this translation helpful? Give feedback.
-
Some of the proposed solutions will clearly work, but they feel dirty because they couple the Surely there must be a better way? Perhaps Turbo can introduce a middleware that analyzes these paths and generates a mapping that's tucked away somewhere much like the cache is. |
Beta Was this translation helpful? Give feedback.
-
I have a similar case. Before turborepo it was enought to add
to tsconfig's compiler options and get rid of all the |
Beta Was this translation helpful? Give feedback.
-
You can checkout my repo on how I approached this setup, not sure it's the best but it seems to work as one would expect. ( The repo is made to reproduce typing issue with tRPC not sure if thats related to the way monorepo handles code sharing) The repository can be found here, there's a readme with brief overview of the directories Feedback is welcome! |
Beta Was this translation helpful? Give feedback.
-
Simply add the paths to Here is how: {
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"...": "other options",
"baseUrl": ".",
"paths": {
"@web/ui": ["../ui/index.tsx"],
"@docs/ui": ["../ui/index.tsx"]
}
},
"exclude": ["node_modules"]
} Usage: import { Button } from "@docs/ui";
import { Button } from "@web/ui"; Note: in this example both Note: 'baseUrl' is required. This works great. Build and VsCode are booth happy. |
Beta Was this translation helpful? Give feedback.
-
While I don't think this is on Turbo to fix, I do wonder why the official docs suggest importing packages through workspaces That had me thinking...do the caching mechanisms break for internal dependencies if you're importing through tsconfig paths? Say I have:
If I make a change to app1, will it rerun tests from ui? I will verify once I get my repo set up correctly but I do wonder why tsconfig imports aren't the defacto recommendation when it results in a much better DX with HMR. |
Beta Was this translation helpful? Give feedback.
-
Just a quick question about doing imports such as: {
"baseUrl": ".",
"compilerOptions": {
"paths": {
"@fe-components/atoms": ["../../packages/fe-components/components/atoms"],
}
},
"extends": "tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
Cause my import alias is pointing to fe-components packages outside of app module, while I can do the same in my component by putting only In my case I'm using turborepo where I have 1 nextjs app and 1 react package(design-system) |
Beta Was this translation helpful? Give feedback.
-
There is a trick that you may or may not have noticed. When you change tsconfig, it is not recognized by VS Code, and you have to restart TypeScript Server. I was facing a similar case when I was working on merging 7 React apps into Turborepo monorepos, and was troubled by the fact that https://marketplace.visualstudio.com/items?itemName=neotan.vscode-auto-restart-typescript-eslint-servers |
Beta Was this translation helpful? Give feedback.
-
Maybe I'm doing something wrong but I don't have problems. This is my setup Turbo + pnpm 7.x
Inside UI i can self reference my own components // packages/ui/src/Screens/Home/index.tsx
import Button from '@ui/src/ds/atoms/Button'
import View from '@ui/src/ds/atoms/View'
export default function Home () {
return (
<View><Button>Click</Button></View>
)
} Am I missing something? I'm not building 👇 {
"name": "@ui",
"version": "0.0.0",
"main": "./src/index.ts",
"sideEffects": false,
"scripts": {
"lint": "eslint \"**/*.ts*\"",
"lint:fix": "lint --fix",
"check-types": "tsc --noEmit",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist"
},
"dependencies": {
"nativewind": "^2.0.11",
"solito": "3.0.0"
},
"peerDependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.0.26",
"eslint": "^7.32.0",
"eslint-config-custom": "workspace:*",
"postcss": "^8.4.21",
"react": "^18.2.0",
"tailwind-config": "workspace:*",
"tailwindcss": "^3.2.4",
"tsconfig": "workspace:*",
"typescript": "^4.9.4"
}
} |
Beta Was this translation helpful? Give feedback.
-
My solution is simply to stop using // ui/components/ButtonB.tsx
import * as React from "react"
import { ButtonA } from "ui/components/ButtonA" // <-- `ui` instead of `~components`
export const ButtonB = () => {
return <ButtonA />
} References don't break, since they are global, so you can do this: // docs/pages/index.tsx
import { ButtonA } from "ui/components/ButtonA" // works as expected I wish this worked out of the box, but it doesn't. You just need to declare the workspace name in every
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"ui/*": ["src/*"]
}
},
} |
Beta Was this translation helpful? Give feedback.
-
Unlike TS configuration, Vite's configuration does not include "/*". This syntax error wasted my entire afternoon. If you are using Vite and encounter errors, you may want to check if there's a mistake in Vite's configuration. {
"compilerOptions": {
"paths": {
"@ui/*": ["../../packages/ui/src/*"]
}
}
}
{
resolve: {
alias: {
'@ui': path.resolve(__dirname, '../../packages/ui/src'),
}
}
} |
Beta Was this translation helpful? Give feedback.
-
Is there a possibility to have an hotreload ? |
Beta Was this translation helpful? Give feedback.
-
I am using "paths": {
"$ui/components": ["./components"],
"$ui/components/*": ["./components/*"],
"$ui/utils": ["./utils"],
"$ui/utils/*": ["./utils/*"]
}
<script lang="ts">
import { cn } from '$ui/utils';
</script>
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [sveltekit(), sconfigPaths({ ignoreConfigErrors: true })],
... https://github.com/xmlking/spectacular/blob/main/apps/console/vite.config.ts |
Beta Was this translation helpful? Give feedback.
-
I'm sorry to bring up an old issue, but I'm trying to figure out how to hot-load new components from my Simple changes seem to work fine when I run
Error:
Is this expected behavior? Do I need to restart Vite each time I add a new component? |
Beta Was this translation helpful? Give feedback.
-
Just wanted to give my solution, if anyone is interested. I didn't use tsconfig and just started using packages.json "imports"
I export every file I want in these indexes files, and then just import them anywhere in your package with (Reason I put it in a index is because of issues with ts/tsx wildcard path, this solve its, making it work similar to tspaths) If your using internal packages make sure your app packages use bundler and esnext in its tsconfig
This all works with VSCode and all types seamlessly work like in tspaths , it will error if an internal package tries to import it. This coupled with doing similar with the exports field ,you can have clear internal paths/files that can never be exported. Edit: Node version is v20.12.2 and typescript version is 5.3.3 |
Beta Was this translation helpful? Give feedback.
-
Hey, folks! We've officially documented our recommendation on this: Use Node.js subpath imports. If you're not able to upgrade to TypeScript 5.4, there are many other options here in this thread that you can choose from that come with their own tradeoffs. I appreciate everyone in this thread who talked through various ways of handling absolute imports in TypeScript monorepos, whether that was asking questions or providing answers. From talking to the Node.js and TypeScript teams, Node.js subpath imports is your best bet going forward! |
Beta Was this translation helpful? Give feedback.
-
Update: too many caveats. See below and replies. I have a working example to solve monorepo imports using subpath import/export as suggested above:
Caveats
UsageThe usage in a example import { Gradient } from '#src/components/atoms/gradient' // this file is actually in `[...]/gradient/index.tsx`, but doesn't need that full path
import { links } from '#src/constants/links'
import { cva, cn } from '#src/utils/theme'
import { Card } from '@repo/ui/components/card' // can retrieve types from `packages/ui/src/components/card` !
import Image from 'next/image'
import '#src/themes/tailwind.css'
import '@repo/ui/themes/tailwind.css' // can retrieve compiled css from `packages/ui/dist/themes/tailwind.css` ! Implementation
"name": "@repo/next-app",
"imports": {
"#src/*": ["./src/*.ts", "./src/*.tsx", "./src/*.d.ts", "./src/*/index.ts", "./src/*/index.tsx", "./src/index.d.ts", "./src/*"]
},
"name": "@repo/ui",
"imports": {
"#src/*": ["./src/*.ts", "./src/*.tsx", "./src/*.d.ts", "./src/*/index.ts", "./src/*/index.tsx", "./src/index.d.ts", "./src/*"]
},
"exports": {
"./*": {
"types": ["./src/*.ts", "./src/*.tsx", "./src/*.d.ts", "./src/*/index.ts", "./src/*/index.tsx", "./src/index.d.ts", "./src/*"],
"default": ["./src/*.ts", "./src/*.tsx", "./src/*.d.ts", "./src/*/index.ts", "./src/*/index.tsx", "./src/index.d.ts", "./dist/*"]
}
},
"exports": {
"./*": {
"types": ["./src/*.ts", "./src/*.tsx", "./src/*.d.ts", "./src/*/index.ts", "./src/*/index.tsx", "./src/index.d.ts", "./src/*"],
"default": ["./dist/*.js", "./dist/*.jsx", "./dist/*/index.js", "./dist/*/index.jsx", "./dist/*"]
}
}, |
Beta Was this translation helpful? Give feedback.
-
A couple important clarifications:
|
Beta Was this translation helpful? Give feedback.
-
I'm still getting this error, FWIW:
"imports": {
"#root/*": "./*"
}
import { signIn, providerMap } from "#root/auth.ts"; |
Beta Was this translation helpful? Give feedback.
Hey, folks! We've officially documented our recommendation on this: Use Node.js subpath imports.
If you're not able to upgrade to TypeScript 5.4, there are many other options here in this thread that you can choose from that come with their own tradeoffs.
I appreciate everyone in this thread who talked through various ways of handling absolute imports in TypeScript monorepos, whether that was asking questions or providing answers. From talking to the Node.js and TypeScript teams, Node.js subpath imports is your best bet going forward!