├── .gitignore
├── README.md
├── changes
└── V1_Requirements.md
├── package.json
├── packages
└── @isomorphic-typescript
│ └── ts-monorepo~
│ ├── .npmignore
│ ├── package.tgz
│ └── source
│ ├── colorize-special-text.ts
│ ├── common
│ ├── console-formatters
│ │ └── config-path.ts
│ ├── constants.ts
│ ├── errors.ts
│ ├── types
│ │ ├── io-ts
│ │ │ ├── config-types.ts
│ │ │ ├── convert-errors.ts
│ │ │ ├── custom-type-helpers.ts
│ │ │ └── exactly.ts
│ │ ├── merged-config.ts
│ │ ├── monorepo-package.ts
│ │ ├── package.d.ts
│ │ │ └── validate-npm-package-name.d.ts
│ │ └── traits.ts
│ └── util.ts
│ ├── file-system
│ ├── object.ts
│ ├── parse-json.ts
│ ├── presence-assertions.ts
│ └── watcher.ts
│ ├── logging
│ ├── log.ts
│ ├── pipe-debug-log.ts
│ └── runtime-type-inference.ts
│ ├── package-dependency-logic
│ ├── berry-install
│ │ └── install-with-berry.ts
│ └── monorepo-package-registry.ts
│ ├── process
│ ├── command-runner.ts
│ ├── parent-child-rpc.ts
│ ├── restart-program.ts
│ └── typescript-runner.ts
│ ├── self-change-detector.ts
│ ├── sync-logic
│ ├── cached-latest-version-fetcher.ts
│ ├── converters
│ │ ├── input-to-merged
│ │ │ ├── files
│ │ │ │ └── package.json.ts
│ │ │ └── package-config.ts
│ │ └── monorepo-to-output
│ │ │ ├── files
│ │ │ ├── monorepo-package.json.ts
│ │ │ ├── package.json.ts
│ │ │ ├── ts-project-leaves.json.ts
│ │ │ └── tsconfig.json.ts
│ │ │ └── write-monorepo-package-files.ts
│ ├── deep-object-compare.ts
│ ├── error-coalesce.ts
│ ├── input-validation
│ │ ├── validate-monorepo-config.ts
│ │ ├── validate-package-config.ts
│ │ ├── validate-package.json.ts
│ │ ├── validate-scope.ts
│ │ ├── validate-templates.ts
│ │ └── validate-tsconfig.json.ts
│ ├── sync-monorepo.ts
│ ├── traverse-package-tree.ts
│ ├── validate-no-unexpected-folders.ts
│ └── writers
│ │ ├── ignore.ts
│ │ └── json.ts
│ ├── ts-monorepo.ts
│ └── webpack
│ ├── webpack-audit-hooks.ts
│ ├── webpack-future-start.ts
│ └── webpack.config.ts
├── prep-safe.js
├── scripts
├── publish.js
└── untar.js
└── ts-monorepo.json
/.gitignore:
--------------------------------------------------------------------------------
1 | tsconfig.tsbuildinfo
2 | package-lock.json
3 | .yarn
4 | .yarnrc.yml
5 | yarn.lock
6 | .pnp.js
7 | .vscode
8 | dist-experiment/
9 | .ts-monorepo
10 | build/
11 | tsconfig.json
12 | package.json
13 | !dist-safe/package.json
14 | bundle/
15 | stable/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a tool which helps automate the config management of monorepos which use Yarn 2 for dependency management and TypeScript project references for incremental (only recompile files which changed) compilation of multiple packages.
2 |
3 | The tool will watch a single configuration file named `ts-monorepo.json` which resides in the project root, and from this file, all the config files of the monorepo packages are derived.
4 |
5 | The file represents a centralized place to store [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) configuration details about all the npm packages within your typescript monorepo. DRY principle is followed by giving users a reusable and composable `templates` field where any number of templates which define config snippets can be defined and used/combined in different specific packages in the monorepo.
6 | This makes it easy to have a standard set of TypeScript strictness used in all projects while only needing to declare it once, a standard set of dependencies and typescript settings which can be used across all nodejs monorepo packages or web packages. Eventually it will become possible to have a cononical template for React Native / React XP / Electron development.
7 |
8 | # How do I use it?
9 |
10 | Install it (Yarn Required):
11 | - `yarn set version berry`
12 | - `yarn add -D @isomorphic-typescript/ts-monorepo`
13 | - `yarn ts-monorepo`
14 |
15 | # How does it work?
16 |
17 | From a high level, upon change detection of `ts-monorepo.json`, this tool will
18 | 1. Validate the config and proceed [iff](https://en.wikipedia.org/wiki/If_and_only_ifs) valid.
19 | 1. Create package folder, required parent folder(s), package.json, & tsconfig.json files if any of these are missing.
20 | 1. Update the existing config files if they were already present.
21 | 1. Update or create a `tsconfig-leaves.json` file, which is a json that will reside in the root of your project, and contains references to all leaf projects (leaf projects are not a dependency of any other package).
22 | 1. Update Yarn 2 `workspaces` field in root-level `package.json` to reference all your packages, and ensure root-level `package.json` is private
23 | 1. Restart a `tsc -b --watch` process that builds all packages referenced in `tsconfig-leaves.json` incrementally, therefore building all the packages in correct order.
24 |
25 | ## It's Opinionated
26 |
27 | This tool is very opinionated in how a monorepo is managed:
28 | 1. TypeScript build watch is used.
29 | 1. TypeScript project references are used.
30 | 1. Changes to individual package.jsons and tsconfig.jsons will be overwritten during the sync process, so individual settings must be controlled via the centralized config.
31 | 1. The project forces single versioning across all packages in the monorepo.
32 | 1. Certain tsconfig compilerOptions will be enabled without your choice. They are: "composite", "declaration", "declarationMap", "sourceMap". The reasoning behind this can be seen [here](https://github.com/RyanCavanaugh/learn-a#tsconfigsettingsjson).
33 |
34 | # VSCode doesn't understand my types!
35 |
36 | This is because ts-monorepo uses Yarn v2 (Berry), which uses Plug-n-Play
37 |
38 | - `yarn add -D typescript`
39 | - `yarn add -D @yarnpkg/pnpify`
40 | - `yarn pnpify --sdk vscode`
41 | - In bottom right corner of VSCode click on the version, switch to pnpify version.
42 | - Install the [ZipFS VSCode extension](https://marketplace.visualstudio.com/items?itemName=arcanis.vscode-zipfs) so you can go to definition for dependencies which are now all in zip files.
43 |
44 | # ts-monorepo.json in detail
45 |
46 | In order to view an example of how to structure `ts-monorepo.json`, please look at the same file in this repo, as ts-monorepo is maintained using ts-monorepo. Also look [here](https://github.com/skoville/webpack-hot-module-replacement/blob/master/ts-monorepo.json) to see how you can use templates. Basically you can declare as many templates as you want which are snippets of reusable config that can be included in the config files of some packages but not others.
47 |
48 | The generated tsconfig.json and package.json files from this tool in each package directory are a [deepmerge](https://www.npmjs.com/package/deepmerge) of all templates it extends and the leaf config for the package except for the following major caveats:
49 |
50 |
51 |
52 |
53 | the behavior of merging a `package.json` file's `dependencies`, `devDependencies`, and `peerDependencies` object is first an array merge to get the combined set of dependencies, then a transformation of this array into a valid npm dependency object where each package name refers to the most up-to-date version of that package.
54 |
55 | For example, this value for `"baseConfigs"`.`"package.json"`.`"devDependencies"` in `ts-monorepo.json`
56 | ```json
57 | [
58 | "typescript",
59 | "react",
60 | "ansicolor"
61 | ]
62 | ```
63 | will be transformed into this valid package.json dependency object in the package's generated package.js file
64 | ```json
65 | {
66 | "ansicolor": "^1.1.89",
67 | "react": "^16.8.6",
68 | "typescript": "^3.5.2"
69 | }
70 | ```
71 |
72 | The generated file will rearrange the entries alphabetically, and you will implicitly keep all dependencies throughout your entire monorepo up to date by using this tool. If a package within the dependency array is equal to a package name managed within your monorepo, then the version will be the monorepo version and Yarn 2 will ensure the workspace version is used rather than the npm version of the package. If you want to specify specific versions you can write out dependencies like this
73 |
74 | ```json
75 | "dependencies": [
76 | ["typescript", "4"],
77 | "react",
78 | "ansicolor"
79 | ]
80 | ```
81 |
82 | which results in the `typescript` package having that specific semver, but the other dependencies being set to the latest versions in the generated `package.json`:
83 |
84 | ```json
85 | "dependencies": {
86 | "ansicolor": "^1.1.89",
87 | "react": "^16.8.6",
88 | "typescript": "4"
89 | }
90 | ```
91 |
92 |
The tsconfig.json files generated contain references that point to dependency projects' relative paths and contain mandatory enabled compiler options that must be used to enable typescript project references to work properly. See the next section for specifics on these options.
93 |
94 |
95 |
96 | Some notes should be said about how the folder structure is setup. Take note of [this project](https://github.com/skoville/webpack-hot-module-replacement/blob/master/ts-monorepo.json). Basically the following in `ts-monorepo.json`
97 |
98 | ```json
99 | "packages": {
100 | "@scope": {
101 | "some": {
102 | "package~": {
103 | ...
104 | }
105 | }
106 | }
107 | }
108 | ```
109 |
110 | results in the following folder structure:
111 |
112 | ```
113 | monorepo-root/
114 | |_packages/
115 | |_@scope/
116 | |_some/
117 | |_package~/
118 | |_package.json
119 | |_tsconfig.json
120 | |_source/
121 | |_...
122 | |_build/
123 | |_...
124 | ```
125 |
126 | The generated package name will be `@scope/somepackage`. You can include separator characters in the json folder segments, and these segment chars will show up in the generated package name but not in the folder names. So for example, we could have the following alternate config in `ts-monorepo.json`
127 |
128 | ```json
129 | "packages": {
130 | "@scope": {
131 | "so": {
132 | ".me": {
133 | "-package~": {
134 | ...
135 | }
136 | }
137 | }
138 | }
139 | }
140 | ```
141 |
142 | and we'd get the following folder structure
143 |
144 | ```
145 | monorepo-root/
146 | |_packages/
147 | |_@scope/
148 | |_so/
149 | |_me/
150 | |_package~/
151 | |_package.json
152 | |_tsconfig.json
153 | |_source/
154 | |_...
155 | |_build/
156 | |_...
157 | ```
158 |
159 | And a package name of `@scope/so.me-package`.
160 |
161 | You need to put a `~` character at the end of the folder segment to mark the termination of a package name which will cause the validation algorithm to ensure the contained attributes make up a valid package config. The tilde shows up in the folder structure to help you identify packages in your IDE, but the tilde doesn't show up in the package name because that's not even a valid package name character.
162 |
163 | Another thing to note is that the first layer under `packages` needs to either be some valid scope (starts with `@`) or the literal string `global-scope` meaning that your package is under no scope.
164 |
165 | I know this setup for packages folder structure is very heavy-handed (too strict). After thinking about this a lot I'll likely be removing all these restrictions in the next iteration as I plan to move in a direction where [git worktrees](https://git-scm.com/docs/git-worktree) are used instead of all the code being in a single repo, where there's a `ts-worktree.json` which specifies how the packages folders map to the various git repos, and then each package is a git repo with its own `ts-workspace.json` which defines the `package.json`, `tsconfig.json`, `.npmignore` and soon many other types of config files. This in some ways is a regression since we have more files again, but on the plus-side we'll move towards having templates be shareable packages, meaning that we can move towards a place where instead of having boilerplate repos which you clone, you just `extends` a popular config package from npm and `ts-monorepo` will set up all the config for you based on that package's published `ts-workspace.json`, and we'll still get all the benefits of ts project references for free. This seems like the most scalable long-term approach for companies with many teams where the idea of a monorepo doesn't really make sense. I'll be using this strategy to publish canonical typescript, react, react-native, and electron `ts-workspace` templates.
166 |
167 | ## Why did ts-monorepo switch to Yarn 2 from Lerna?
168 |
169 | Lerna is too problematic in the way it manages a separate `node_modules` folder for each monorepo package, and will allow any monorepo package to use a dependency if it's a part of the monorepo's root `node_modules`. Meanwhile Yarn 2 perfectly hoists all packages across the monorepo, never stores a duplicate copy of a pakcage, and strictly ensures that a program only has access to the dependencies explicitly declared in package.json as a direct or transitive dependency. Yarn 2 is clearly the future, and Plug n Play adoption should be encouraged.
170 |
171 | ## Nice benefits
172 |
173 | 1. Now all of your configs are generated from this one `ts-monorepo.json` file, and so the tsconfig.json and package.json files can go into `.gitignore` since they are now all managed/generated automatically as part of the build, watch process, leading to a cleaner repo.
174 | 1. Now new package setup in the monorepo is very quick; just add a new entry to config file's `packages` object and the tool which watches the config file for saves will create all the folders, install dependencies, and add it to the incremental build process as you update the entry. Essentially this is declarative programming of all the monorepo's build configuration and dependency installation/wiring.
175 | 1. This is a better alternative to tsconfig's own extends functionality, because:
176 | 1. All items are inherited, not just `compilerOptions`
177 | 1. Arrays are unioned together rather than the child's array replacing the parent config's array, leading to less config repetition.
178 |
179 | ## Any Examples?
180 |
181 | I created this project to manage [skoville/webpack-hot-module-replacement](https://github.com/skoville/webpack-hot-module-replacement). Notice in that project how there is just one package.json and a ts-monorepo.json in the root of the project, and how besides those two config files, there are no others throughout the remainder of the monorepo. This is great! And the autosyncing on every ts-monorepo.json change saves me a great deal of time.
182 |
183 | ## Maintainer's Quick Start
184 |
185 | If you want to submit a PR to improve this project, then after cloning
186 |
187 | ```
188 | git clone git@github.com/isomorphic-typescript/ts-monorepo
189 | ```
190 |
191 | run
192 |
193 | ```bash
194 | yarn add -D typescript
195 | yarn add -D @yarnpkg/pnpify
196 | yarn pnpify --sdk vscode
197 | ```
198 |
199 | then run `yarn build:stable` for an initial install and build of the rapid package.
200 | After the build is successful, stop the stable build process and start running `yarn build:rapid` instead so the version of tsmonorepo being used to build tsmonorepo is the version you are actively modifying. This way you can test your changes real-time as you code. If you ever make a mistake in rapid mode, you can always revert back to stable build mode until your modifications are functional again.
201 |
202 | If the stable command initially fails you may need to temporarily remove this entry in the package.json:
203 |
204 | ```json
205 | "@isomorphic-typescript/ts-monorepo": "portal:./packages/@isomorphic-typescript/ts-monorepo~",
206 | ```
207 |
208 | then run `yarn install`, then run `yarn build:stable`
209 |
210 | Once you are satisfied with your changes, submit a PR. We have a github action which will automatically run the packaging and publishing steps if the PR is merged.
211 |
212 | ## Maintainers' Tenets
213 |
214 | 1. The entire interface of this tool will be through options within the ts-monorepo.json file, meaning the CLI will never have any arguments or take parameters through the command prompt. No exceptions to this rule shall ever be allowed. Think hard about out how you can add feature **XYZ** through a new json option instead. Hold up your right hand and repeat after me:
215 |
216 | > As a maintainer, I vow to reject PRs which try to add command line arguments.
217 |
218 | ## TODO
219 |
220 | 1. create VSCode extension which understands this config file, showing errors, auto suggesting values, and click to go to npm or other package support.
221 | 1. Create a demo gif for the README.
222 | 1. Improve quality of error messages
223 | 1. Support independent versioning? Not sure if this is a good feature or not.
224 | 1. Make the sync protocol more generic so as to support any arbitrary config or make it easy for maintainers to PR for new types of config files.
225 | 1. Move towards git worktrees, and have each package publish its own `ts-workspace.json` so that published packages can share config which can become an alternative standard which replaces boilerplate code repos.
--------------------------------------------------------------------------------
/changes/V1_Requirements.md:
--------------------------------------------------------------------------------
1 | # V1 Requirements/Roadmap.
2 |
3 | This document outlines the planned changes from V0 to V1.
4 |
5 | ## Initial added features
6 |
7 |
8 |
9 |
10 |
Done?
11 |
Description
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | When specifying dependencies, either include the name in the array for the tool to determine latest version, or put an object in the array which allows you to define the version.
20 |
21 | ```typescript
22 | type NodeDependency = string | {package: string, version: string};
23 | ```
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ts-monorepo will take over the bootstrap and hoist responsibilities from lerna, such that there is only one instance of each package stored on disk between all projects of the monorepo. It will also use symlinking such that the node_modules folder of each project reflects how it would be structured after a normal npm install rather than linked projects containing a node_modules which has all its dependencies and dev dependencies inside
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Now instead of having base configs of tsconfig.json and package.json only, it should be basePackageConfigs which is a map from template name to template (renaming this option from basePackageConfigs to templates). Each template then has configs for tsconfig.json, package.json, other config files/objects, plus options unique to a ts-monorepo package. Having multiple templates means that package config must now extend its base explicitly by name (or extend nothing). Each package config will have an extends field which is an ordered array of template names. The templates will be applied in the given order, with later entries overwriting earlier ones during deep merge. Finally the package config itself will act as the final overwrite. In addition to package configs being able to extend templates, templates will also be able to extend other templates. ts-monorepo will ensure that no circular template dependencies exist.
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | For cases where many packages begin with the same prefix (i.e. "webpack-hmr-one", "webpack-hmr-two", "webpack-hmr-three", etc.), a hierarchy of folders may now be used to organize the monorepo on disk. Under monorepo an object will either be a SubPackageConfig or a PackageConfig. A SubPackageConfig will have the fields children, delimiter, and package. children attribute is a map from name to more package or subpackage configs. delimiter is an option within a subpackage config which will be a series of characters which are used to join the existing package name up until that point with the names of the package configs belonging to the given subpackage config. Finally, package allows for the monorepo to have a package named my-project and my-project-helpers at the same time. Note that this config strategy allows for the possibility of multiple config structures to resolve to the same package name. In these cases ts-monorepo will ensure each package name is only resolved to once, also taking into account npm's moniker rules when evaluating equivalence.
51 |