This is an alpha, sneak peek of Monorepo Maestros. For this iteration, I'm getting all of my thoughts down. In the future, we'll have better information architecture, graphics, and other awesomeness. Your feedback is welcome!

Internal Package

Internal Packages are similar to External Packages in many ways:

However, Internal Packages are different in two important ways:

When you compile a package in your Turborepo, you can cache your compilation outputs for sub-second build times. Build your source code once; never build it again.

Let's take a look at what it takes to build an Internal Package for UI component library with React and TypeScript.

Create a workspace

We first need to lay the foundation for our workspace. We can do so in a few quick steps.

Make sure our package manager sees the workspace

Establish a workspace by following the workspace instructions for your package manager.

Add our tsconfig.json

We'll be making a TypeScript package so we need to have a tsconfig.json. If you've read the Typescript reference already, this will look familiar:

packages/ui/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/tsconfig/react-library.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"outDir": "dist"
},
"include": ["."],
"exclude": ["dist", "node_modules"]
}
packages/ui/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/tsconfig/react-library.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"outDir": "dist"
},
"include": ["."],
"exclude": ["dist", "node_modules"]
}

There are a few important properties that you'll want to make sure you include in either your base tsconfig or here in your package's tsconfig.json to have the smoothest experience:

All of these configurations will add up to a perfect TypeScript experience once we get to our compilation step.

Add a package.json

Create a package.json in your workspace directory. We'll fill in "exports" and "scripts" step-by-step later so we can understand what they do, piece-by-piece.

packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.1.0", // Arbitrary for Internal Packages
"private": true, // Prevents npm publishing
"exports": {}, // Entrypoints for your application
"scripts": {}, // Tasks for the package
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.0",
"tsup": "^7.2.0", // Bundler for compilation
"typescript": "^5.1.6"
}
}
packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.1.0", // Arbitrary for Internal Packages
"private": true, // Prevents npm publishing
"exports": {}, // Entrypoints for your application
"scripts": {}, // Tasks for the package
"dependencies": {
"react": "^18.2.0"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.0",
"tsup": "^7.2.0", // Bundler for compilation
"typescript": "^5.1.6"
}
}

At this point, hit the install command for your package manager to get the packages installed.

Create some source code

We'll use an src folder in our workspace to keep things tidy. Create your first UI component in an index file:

packages/ui/src/index.tsx
export const Button = (props: { text: string }) => {
return <button>{props.text}</button>;
};
packages/ui/src/index.tsx
export const Button = (props: { text: string }) => {
return <button>{props.text}</button>;
};

Prepare for compilation

We'll need to get our TypeScript compiled to JavaScript so our applications and packages can consume our code. For this, we like to use tsup, a bundler that gives us a simple yet powerful bundling API with esbuild under the hood.

Make a tsup configuration file

We want to be able to compile our package the same way whether we are building in watch mode for development or building for production. tsup can read from a tsup.config.js file to give us a consistent configuration to use:

packages/ui/tsup.config.js
import { defineConfig } from 'tsup';
import { exec } from 'child_process';

export default defineConfig((options) => ({
entry: ['src/index.tsx'], // Where your source code lives (can provide multiple entries)
splitting: false,
treeshake: true,
clean: true,
outDir: 'dist', // Where you want your compiled files to live
onSuccess: async () => {
exec('tsc --emitDeclarationOnly');
},
...options,
}));
packages/ui/tsup.config.js
import { defineConfig } from 'tsup';
import { exec } from 'child_process';

export default defineConfig((options) => ({
entry: ['src/index.tsx'], // Where your source code lives (can provide multiple entries)
splitting: false,
treeshake: true,
clean: true,
outDir: 'dist', // Where you want your compiled files to live
onSuccess: async () => {
exec('tsc --emitDeclarationOnly');
},
...options,
}));

Note: We're using JavaScript here but will still get great autocomplete. 😄

Notice the important onSuccess function here. It's responsible for giving us TypeScript declaration files so that our editor will jump to our TypeScript source code instead of our compiled JavaScript. tsup is unable to generate these files but, luckily, the TypeScript compiler handles these pretty quickly.

Make a build and dev script

Head back to your package.json and add some scripts:

packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.1.0", // Arbitrary for Internal Packages
"private": true, // Prevents npm publishing
"exports": {}, // Entry points for your application
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"dependencies": {...},
"devDependencies": {...}
}
packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.1.0", // Arbitrary for Internal Packages
"private": true, // Prevents npm publishing
"exports": {}, // Entry points for your application
"scripts": {
"build": "tsup",
"dev": "tsup --watch"
},
"dependencies": {...},
"devDependencies": {...}
}

Ensure your dist will be gitignored

You'll want to make sure you aren't committing your builds to source control. If you haven't done so yet, add dist to your .gitignore:

Set up your exports

exports is where we define the entry points to our package. Workspaces that consume your Internal Package will use these as the places that they can import code from.

To create the entrypoint, we'll fill in the "exports" field of the package.json:

packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.1.0",
"private": true,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup -w"
},
"dependencies": {...},
"devDependencies": {...}
}
packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.1.0",
"private": true,
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup",
"dev": "tsup -w"
},
"dependencies": {...},
"devDependencies": {...}
}

Compile your code

We should be ready now! Run turbo build --filter=ui in your terminal and check your new ui package to find a dist folder full of .d.ts and .js files.

We'll now be able to import our code in an application once we install it to its workspace:

apps/web/src/index.ts
import { Button } from '@repo/ui';
apps/web/src/index.ts
import { Button } from '@repo/ui';

Iteration speed: Unlocked

You'll now be able to continually update this package much faster than if you needed to publish it to npm since it will be right there alongside the rest of the code for your applications. If you, for example, change a TypeScript interface in your UI package, you'll instantly be able to find any mistypings in your application the next time you run turbo typecheck.

If you'd like to build conformance into your new UI package, be sure to follow along with the references in the Guardrails pages.