66- [ Features] ( #features )
77- [ Install] ( #install )
88- [ Usage] ( #usage )
9- - [ Workspace ] ( #workspace )
9+ - [ Monorepo Setup ] ( #monorepo-setup )
1010 - [ Namespace] ( #namespace )
1111 - [ Packages] ( #packages )
1212 - [ Apps] ( #apps )
1313 - [ Services] ( #services )
14- - [ The "built packages" strategy] ( #the-built-packages-strategy )
15- - [ Convert path aliases] ( #convert-path-aliases )
16- - [ Write ESM without import file extensions] ( #write-esm-without-import-file-extensions )
17- - [ Tree shaking] ( #tree-shaking )
18- - [ The "internal packages" strategy] ( #the-internal-packages-strategy )
19- - [ Live code changes from internal packages] ( #live-code-changes-from-internal-packages )
2014- [ Firebase] ( #firebase )
2115 - [ Demo Project] ( #demo-project )
2216 - [ Deploying] ( #deploying )
2721
2822## Introduction
2923
24+ This is a personal quest for the perfect Typescript monorepo setup.
25+
3026> There is an accompanying article
3127> [ "My quest for the perfect TS monorepo"] ( https://thijs-koerselman.medium.com/my-quest-for-the-perfect-ts-monorepo-62653d3047eb )
3228> that you might want to read for context.
3329
34- This is a personal quest for the perfect Typescript monorepo setup.
35-
3630It is the best I could come up with given the tooling that is available, so
3731expect this repository to change over time as the ecosystem around Typescript
3832evolves.
@@ -59,20 +53,19 @@ and I recommend reading
5953
6054- [ Turborepo] ( https://turbo.build/ ) to orchestrate the build process and
6155 dependencies, including the v2 watch task.
62- - Showcasing a traditional "built package" with multiple entry points, as well
63- as the [ "internal package"] ( #the-internal-packages-strategy ) strategy
64- referencing Typescript code directly.
6556- Multiple isolated Firebase deployments, using
6657 [ firebase-tools-with-isolate] ( https://github.com/0x80/firebase-tools-with-isolate )
6758- Firebase emulators with hot reloading
6859- A web app based on Next.js with [ ShadCN] ( https://ui.shadcn.com/ ) and
6960 [ Tailwind CSS] ( https://tailwindcss.com/ )
7061- Working IDE go-to-definition and go-to-type-definition using ` .d.ts.map ` files
71- - ES modules for everything
62+ - ESM everything
7263- Path aliases
7364- Shared configurations for ESLint
74- - Simple standardized configuration for TypeScript
65+ - Simple standard configuration for TypeScript
7566- Vitest
67+ - Clean, strongly-typed Firestore code for both React (using
68+ ` @typed-firestore/react ` ) and Node.js (using ` @typed-firestore/server ` )
7669
7770## Install
7871
@@ -112,7 +105,11 @@ UI on http://localhost:4000.
112105You should now have a working local setup, in which code changes to any package
113106are picked up.
114107
115- ## Workspace
108+ ## Monorepo Setup
109+
110+ > There is an accompanying article
111+ > [ "My quest for the perfect TS monorepo"] ( https://thijs-koerselman.medium.com/my-quest-for-the-perfect-ts-monorepo-62653d3047eb )
112+ > that you might want to read for context.
116113
117114### Namespace
118115
@@ -143,127 +140,13 @@ clear, but I went with `@repo` because I expect it will become the standard.
143140- [ api] ( ./services/api ) A 2nd gen Firebase function (based on Cloud Run) serving
144141 as an API endpoint. This package also illustrates how to use secrets.
145142
146- ## The "built packages" strategy
147-
148- In a traditional monorepo setup, each package exposes its code as if it was a
149- published NPM package. For Typescript this means the code has to be transpiled
150- and the manifest entry points reference to the build output files. You can use
151- Typescript ` tsc ` compiler for this, but it is likely you will want to use a
152- bundler for the reasons explained below.
153-
154- The ` services ` in this codebase use TSUP as a bundler. It is a modern, simple to
155- use Rollup-inspired bundler for Typescript.
156-
157- The advantages of using a bundler are discussed below.
158-
159- ### Convert path aliases
160-
161- If you use path aliases like ` ~/* ` or ` @/* ` to conveniently reference top-level
162- folders from deeply nested import statements, these paths are not converted by
163- the standard Typescript ` tsc ` compiler.
164-
165- A bundler will typically remove path aliases, because it combines your code into
166- self-contained files that do not import other local files themselves.
167-
168- If you target platforms without using bundler, you will have to convert them.
169- You can run something like ` tsc-alias ` after your build step. Note that path
170- aliases can also end up in ` d.ts ` files.
171-
172- ### Write ESM without import file extensions
173-
174- A bundler will allow you to output ESM-compatible code without having to adhere
175- to the ESM import rules. ESM requires all relative imports to be explicit,
176- appending a ` .js ` file extension for importing TS files and appending
177- ` /index.js ` when importing from the root of a directory.
178-
179- The reason you need to use ` .js ` and not ` .ts ` is because these imports, like
180- path aliases are not converted by the Typescript compiler, and so at runtime the
181- transpiled JS file is what is getting imported.
182-
183- Because a bundler, by nature, will bundle code into one or more isolated files,
184- those files do not use relative imports and only contain imports from
185- ` node_modules ` , which do not require a file extension. For this reason, a
186- bundled js file that uses import and export keywords is an ES module.
187-
188- An advantage of writing your code as ESM is that you can import both ES modules
189- and CommonJS without conversion. An application that uses CJS can not import ESM
190- directly, because CJS imports are synchronous and ESM imports are asynchronous.
191-
192- Not having to use ESM import extensions can be especially valuable if you are
193- trying to convert a large codebase to ESM, because I have yet to find a solution
194- that can convert existing imports. There is
195- [ this ESLint plugin] ( https://github.com/solana-labs/eslint-plugin-require-extensions )
196- that you could use it in combination with the --fix flag to inject the
197- extensions, but at the time of writing it does not understand path aliases.
198-
199- ### Tree shaking
200-
201- Some bundlers like TSUP are capable of eliminating dead code by tree-shaking the
202- build output, so that less code remains to be deployed. Eliminating dead code is
203- most important for client-side code that is shipped to the user, but for the
204- core it can also reduce cold-start times for serverless functions, although in
205- most situations, it is probably not going to be noticeable.
206-
207- ## The "internal packages" strategy
208-
209- The
210- [ internal packages] ( https://turbo.build/blog/you-might-not-need-typescript-project-references )
211- strategy, as it was coined by Jared Palmer of Turborepo, removes the build step
212- from the internal packages by linking directly to the Typescript source files in
213- the package manifest.
214-
215- There are some advantages to this approach:
216-
217- - Code and type changes can be picked up directly, removing the need for a watch
218- task in development mode.
219- - Removing the build step reduces overall complexity where you might otherwise
220- use a bundler with configuration.
221- - IDE go-to-definition, in which cmd-clicking on a reference takes you to the
222- source location instead of the typed exports, works without the need for
223- Typescript project references or generating ` d.ts.map ` files.
224-
225- But, as always, there are also some disadvantages you should be aware of:
226-
227- - You can not publish the shared packages to NPM, as you do not expose them as
228- Javascript.
229- - If you use path aliases like ` ~/ ` , you will need to make sure every package
230- has its own unique aliases. You might not need aliases, because shared
231- packages typically do not have a deeply nested folder structure anyway.
232- - Since all source code gets compiled by the consuming application, build times
233- can start to suffer when the codebase grows. See
234- [ caveats] ( https://turbo.build/blog/you-might-not-need-typescript-project-references#caveats )
235- for more info.
236- - The shared package is effectively just a source folder, and as a whole it
237- needs to be transpiled and bundled into the consuming package. This means that
238- its dependencies must also be available in the consuming package. Next.js can
239- do this for you with the ` transpilePackage ` setting, but this is the reason
240- ` services/api ` includes ` remeda ` , as it is used by ` packages/common ` .
241-
242- For testing and comparison, mono-ts uses the internal packages approach for
243- ` @repo/common ` and a traditional built approach for ` @repo/core ` . Both are
244- compatible with ` isolate-package ` for deploying to Firebase.
245-
246- ## Live code changes from internal packages
247-
248- Traditionally in a monorepo, each package is treated similarly to a released NPM
249- package, meaning that the code and types are resolved from the built "dist"
250- output for each module. Adding new types and changing code in shared packages
251- therefore requires you to rebuild these, which can be cumbersome during
252- development.
253-
254- Turborepo does not (yet) include a watch task, so
255- [ Turbowatch] ( https://github.com/gajus/turbowatch ) was created to solve this
256- issue. I haven't tried it but it looks like a neat solution. However, you might
257- want to use the
258- [ internal packages strategy instead] ( #the-internal-packages-strategy ) .
259-
260143## Firebase
261144
262145In their
263146[ documentation for monorepos] ( https://firebase.google.com/docs/functions/organize-functions?gen=2nd#managing_multiple_source_packages_monorepo ) ,
264147Firebase recommends putting all configurations in the root of the monorepo. This
265- makes it possible to deploy all packages at once, and most importantly, run the
266- emulators shared on common ports .
148+ makes it possible to deploy all packages at once, and easily start the emulators
149+ shared between all packages .
267150
268151### Demo Project
269152
0 commit comments