ESBuild TypeScript Turborepo Monorepo starter/example

Details:

This is an example monorepository using ESBuild for it’s near-instantaneous build times and Turborepo for it’s caching capabilities. It’s pre-configured for TypeScript (2 different configurations for browser and for node) and ESLint for linting.

Additionally it’s using NPM Workspaces, most examples I could find online were using YARN.

Installation:

git clone https://github.com/barca-reddit/typescript-vscode-esbuild.git

cd typescript-vscode-esbuild

npm run watch

Tech stack:

Exporting/sharing packages:

NB: I don’t know if this is the best or the accepted way to do this, neither I consider myself an expert, so PR/issues/feedback of any kind is welcome.

To create a shared package and import it somewhere else in your monorepo, edit the contents of package.json of the package you want to export and add the following fields:

"exports": {
    ".": {
        "import": "./out/main.js"
    }
},
"typesVersions": {
    "*": {
        "*": ["./src/main.ts"]
    }
}

The exports field is there to serve plain javascript imports and it should point out to an index (main) file in your compiled out directory. The import nested key is a “conditional export”.

The typesVersions is there to make TypeScript happy and should point out to a file that exports other files (an index). This allows you to do the following:

// foo.ts
export const foo = "foo";

// main.ts
export * from "./foo.js";
export * from "./bar.js";

// inside some other package
import { foo } from "@repo/shared";

Don’t forget to add the package you’re exporting as a dependency to the package you’re importing it to:

// package.json
{
    // ...
    "dependencies": { "@repo/shared": "*" }
}

For more advanced usages, you can also use “subpath exports”.

"exports": {
    ".": {
        "import": "./out/main.js"
    },
    "./other": {
        "import": "./out/other/index.js"
    }
},
"typesVersions": {
    "*": {
        "*": ["./src/main.ts"],
        "other": ["./src/other/index.ts"]
    }
}

This allows you to do the following:

// src/other/foo.ts
export const foo = "foo";

// src/other/bar.ts
export const bar = "bar";

// src/other/index.ts
export * from "./foo.js";
export * from "./bar.js";

// inside some other package
import { foo, bar } from "@repo/shared/other";
//                                     ^^^^^

Notes:

Turborepo

For Turborepo caching to work, it’s essential that all .cache directories it creates are git-ignored.

If build order isn’t important for your setup, add the --parallel flag to the npm build script to speed up compiling. You can probably get away with this if you don’t bundle any code via bundle: true setting passed to esbuild.

TSC

The TypeScript compiler is used only for type checking, everything else is handled by ESBuild.

Typescript/Eslint

TypeScript and ESLint configurations are matter of personal preference and can easily be adjusted to one’s requirements. The same applies for ESBuild, you can also pass additional parameters to buildBrowser or buildNode which will override the default ones.

VSCode

If the .cache directories become annoying, you can just hide them in VSCode, create/edit this file under .vscode/settings.json.

{
    "files.exclude": {
        "cache/": true,
        "**/.turbo": true
    }
}

Version mismatches

You can quickly check whether your package dependencies are in sync, e.g, @repo/a and @repo/b are different versions of the same library.

// package.json (repo a)
{
    "name": "repo/a",
    "dependencies": {
        "foo": "^1.0.0"
    }
}
// package.json (repo b)
{
    "name": "repo/b",
    "dependencies": {
        "foo": "^2.0.0"
    }
}

npm run mismatch

Error: Found version mismatch for the following package:

foo - versions: ^1.0.0, ^2.0.0
- apps/package-a/package.json (@repo/a) - ^1.0.0
- apps/package-b/package.json (@repo/b) - ^2.0.0

This is just a quick and dirty solution that will only report mismatches but won’t fix them for you. For more advanced solutions, check out syncpack.

Useful resources:

GitHub

View Github