├── .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 |
  1. 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 |
  2. 92 |
  3. 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 |
  4. 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 | 11 | 12 | 13 | 14 | 15 | 18 | 25 | 26 | 27 | 28 | 31 | 34 | 35 | 36 | 37 | 40 | 43 | 44 | 45 | 46 | 49 | 52 | 53 | 54 |
Done?Description
16 | 17 | 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 |
29 | 30 | 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 |
38 | 39 | 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 |
47 | 48 | 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 |
55 | 56 | On top of these new features I plan on removing the following: 57 | 58 | 1. No more `packageRoot` option. It will be `packages` without the ability to change that. 59 | 1. No more `publishDistributionFolder` option. This will be enabled for all packages. 60 | 1. The `rootDir` and `outDir` compiler options in tsconfig can no longer be customized. They will automatically be `./source` and `./distribution` respectively. Also `include` will be auto forced to contain `"source/**/*"` 61 | 62 | The theme here is embracing convention over configuration, without limiting possibilities of what can be done with other tools. With that in mind here's one more convention which will be added: 63 | 64 | 1. The first level under `packages` will be a map from scope name to `PackageConfig|SubpackageConfig`; the first level may not contain package names, only scope names. For any package which should not be under any scope, it will be mandated that the literal string `global-scope` is used. 65 | 66 | ## Features which will not immediately be part of V1: 67 | 68 | 1. Give packages the ability to set their own version if desired. 69 | 1. Add VSCode extension to recognize the structure, allow comments, validate package names, and have go to link from package name to reference on npm, within config file, or within monorepo. Have a clickable link next to monorepo versions to bump up by patch, minor, or major version, and show how that version in the monorepo config file differs from what is currently published to npm. 70 | 1. Plugin support allowing for both autocomplete of different values and different config file types, like yaml, toml, ini, xml 71 | 72 | ## What will ts-monorepo.json look like after this is all over? 73 | 74 | Here's an example which includes the described changes 75 | 76 | ```jsonc 77 | { 78 | "ttypescript": true, 79 | "cleanBeforeCompile": false, 80 | "packageTemplates": { 81 | "default": { 82 | "configs": { 83 | "package.json": { 84 | "version": "0.1.2", 85 | "author": "Alexander Leung", 86 | "license": "MIT", 87 | "dependencies": [ 88 | "ansicolor", 89 | { "package": "react", "version": "^13" } 90 | ], 91 | "devDependencies": [ 92 | "@types/node" 93 | ] 94 | }, 95 | "tsconfig.json": { 96 | "compilerOptions": { 97 | "module": "commonjs", 98 | "target": "es6", 99 | "lib": ["esnext"], 100 | "rootDir": "./source" 101 | }, 102 | "types": ["node"] 103 | }, 104 | "someothercustomconfig.json": { 105 | "somestuff": "something" 106 | } 107 | } 108 | }, 109 | "distribuedFromDistribution": { 110 | "extends": ["default"], 111 | "configs": { 112 | "tsconfig.json": { 113 | "compilerOptions": { 114 | "outDir": { 115 | "path": "./distribution", 116 | "publishFromHere": true 117 | } 118 | } 119 | } 120 | }, 121 | "copyFiles": { 122 | "package.json": "distribution/", 123 | ".npmignore": "distribution/", 124 | "someothercustomconfig.json": "distribution/configwithanewname.json", 125 | "source/someimportantdatafile.txt": "distribution/data.txt" 126 | }, 127 | "deleteConfig": [ 128 | "[configs][someothercustomconfig]", 129 | "[configs][package.json][devDependencies][@types/node]", 130 | "[configs][package.json][dependencies][[package]react]" 131 | ] 132 | } 133 | }, 134 | "packages": { 135 | "project1": { 136 | "extends": "default", 137 | "configs": { 138 | "package.json": { 139 | "files": ["distribution"], 140 | "main": "./distribution/package.js", 141 | "types": "./distribution/package.d.ts", 142 | "description": "some package for node", 143 | "scripts": { 144 | "custom": "project2-command input" 145 | }, 146 | "devDependencies": [ 147 | "@somescope/project2" 148 | ] 149 | } 150 | } 151 | }, 152 | "@somescope/project2": { 153 | "extends": "distribuedFromDistribution", 154 | "configs": { 155 | "package.json": { 156 | "description": "some command line tool for node", 157 | "bin": { 158 | "project2-command": "./distribution/project2-command.js" 159 | }, 160 | "dependencies": [ 161 | "@types/node" 162 | ] 163 | } 164 | } 165 | } 166 | } 167 | } 168 | ``` 169 | 170 | The resulting directory structure would look like this: 171 | 172 | ``` 173 | 174 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "author": "Alexander Leung", 4 | "license": "MIT", 5 | "scripts": { 6 | "clean": "shx rm -rf ./.yarn ./packages/@isomorphic-typescript/ts-monorepo~/build && shx rm ./packages/@isomorphic-typescript/ts-monorepo~/tsconfig.tsbuildinfo ./.pnp.js ./.yarnrc.yml ./yarn.lock", 7 | "gen:pack": "shx cp ./README.md ./packages/@isomorphic-typescript/ts-monorepo~/ && yarn workspace @isomorphic-typescript/ts-monorepo pack && shx rm ./packages/@isomorphic-typescript/ts-monorepo~/README.md", 8 | "gen:stable": "shx rm -rf ./stable && shx mkdir -p ./stable && node ./scripts/untar.js ./packages/@isomorphic-typescript/ts-monorepo~/package.tgz ./stable", 9 | "build:stable": "yarn gen:stable && yarn add -D @isomorphic-typescript/ts-monorepo@portal:./stable && yarn install && yarn ts-monorepo", 10 | "build:rapid": "yarn add -D @isomorphic-typescript/ts-monorepo@portal:./packages/@isomorphic-typescript/ts-monorepo~ && yarn install && yarn start:rapid", 11 | "start:rapid": "node ./packages/@isomorphic-typescript/ts-monorepo~/build/ts-monorepo.js", 12 | "build:compile-only": "tsc -b --watch --preserveWatchOutput ./.ts-monorepo/tsconfig-leaves.json", 13 | "vscode": "yarn add -D @yarnpkg/pnpify && yarn pnpify --sdk && yarn remove @yarnpkg/pnpify", 14 | "publish:npm": "npm login && npm publish --access public ./packages/@isomorphic-typescript/ts-monorepo~/package.tgz", 15 | "publish:beta": "publish:npm && yarn gen:stable && node ./scripts/publish.js next", 16 | "publish:release": "publish:npm && yarn gen:stable && node ./scripts/publish.js latest", 17 | "publish:both": "yarn gen:stable && node ./scripts/publish.js next latest" 18 | }, 19 | "devDependencies": { 20 | "@isomorphic-typescript/ts-monorepo": "portal:./packages/@isomorphic-typescript/ts-monorepo~", 21 | "@yarnpkg/pnpify": "^3.0.0-rc.6", 22 | "nodemon": "^2.0.7", 23 | "shx": "^0.3.2", 24 | "tar": "^6.0.2", 25 | "ttypescript": "^1.5.12", 26 | "typescript": "^4.3.2" 27 | }, 28 | "dependenciesMeta": { 29 | "@types/webpack": { 30 | "unplugged": true 31 | }, 32 | "fp-ts": { 33 | "unplugged": true 34 | }, 35 | "io-ts": { 36 | "unplugged": true 37 | }, 38 | "webpack": { 39 | "unplugged": true 40 | } 41 | }, 42 | "private": true, 43 | "workspaces": [ 44 | "packages/@isomorphic-typescript/ts-monorepo~" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/.npmignore: -------------------------------------------------------------------------------- 1 | source 2 | tsconfig.json 3 | tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/package.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/isomorphic-typescript/ts-monorepo/3affe431782d326ea01748bb04b0d55d90a6f8da/packages/@isomorphic-typescript/ts-monorepo~/package.tgz -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/colorize-special-text.ts: -------------------------------------------------------------------------------- 1 | import * as ansicolor from 'ansicolor'; 2 | export const colorize = { 3 | file: ansicolor.lightGreen, 4 | symlink: ansicolor.lightBlue, 5 | directory: ansicolor.cyan, 6 | scope: ansicolor.yellow, 7 | package: ansicolor.lightMagenta, 8 | command: ansicolor.lightBlue, 9 | error: ansicolor.red, 10 | template: ansicolor.lightMagenta, 11 | type: ansicolor.lightCyan, 12 | badValue: ansicolor.lightRed, 13 | subfeature: ansicolor.cyan 14 | }; -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/console-formatters/config-path.ts: -------------------------------------------------------------------------------- 1 | import * as ansicolor from 'ansicolor'; 2 | 3 | export function constructPresentableConfigObjectPath(configObjectPath: string[]): string { 4 | return configObjectPath.map(key => `${ansicolor.lightYellow('[')+ansicolor.lightCyan(key)+ansicolor.lightYellow(']')}`).join(""); 5 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | export const PACKAGE_JSON_FILENAME = "package.json"; 3 | export const TOOL_PACKAGE_JSON = require(`../../${PACKAGE_JSON_FILENAME}`); 4 | export const TS_BUILD_INFO_FILENAME = "tsconfig.tsbuildinfo"; 5 | export const TOOL_FULL_NAME = TOOL_PACKAGE_JSON.name as string; 6 | export const TOOL_SHORT_NAME = TOOL_FULL_NAME.split('/').pop() ?? "test"; // Works if either package name is scoped or not. 7 | export const TOOL_VERSION = TOOL_PACKAGE_JSON.version as string; 8 | export const CONFIG_FILE_NAME = `${TOOL_SHORT_NAME}.json`; 9 | export const CONFIG_FILE_RELATIVE_PATH = `${CONFIG_FILE_NAME}`; 10 | export const CONFIG_FILE_ABSOLUTE_PATH = path.resolve(CONFIG_FILE_RELATIVE_PATH); 11 | export const MONOREPO_PACKAGE_JSON_RELATIVE_PATH = PACKAGE_JSON_FILENAME; 12 | export const MONOREPO_PACKAGE_JSON_ABSOLUTE_PATH = path.resolve(MONOREPO_PACKAGE_JSON_RELATIVE_PATH); 13 | export const TS_MONOREPO_FOLDER_RELATIVE_PATH = `.${TOOL_SHORT_NAME}`; 14 | export const TS_MONOREPO_FOLDER_ABSOLUTE_PATH = path.resolve(TS_MONOREPO_FOLDER_RELATIVE_PATH); 15 | export const TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_NAME = "tsconfig-leaves.json"; 16 | export const TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_RELATIVE_PATH = `${TS_MONOREPO_FOLDER_RELATIVE_PATH}/${TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_NAME}`; 17 | export const TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_ABSOLUTE_PATH = path.resolve(TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_RELATIVE_PATH); 18 | export const PACKAGES_DIRECTORY_NAME = "packages"; 19 | export const PACKAGES_DIRECTORY_RELATIVE_PATH = `${PACKAGES_DIRECTORY_NAME}`; 20 | export const PACKAGE_NAME_CONFIG_PATH_REQUIRED_SUFFIX = "~"; 21 | export const GLOBAL_SCOPE_NAME = "global-scope"; 22 | export const TS_CONFIG_JSON_FILENAME = "tsconfig.json"; 23 | export const TS_CONFIG_JSON_OUT_DIR = "./build"; 24 | export const TS_CONFIG_JSON_ROOT_DIR = "./source"; 25 | export const SUCCESS = Symbol("Success"); 26 | export type Success = typeof SUCCESS; -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/errors.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorType { 2 | InvalidScope = "InvalidScope", 3 | InvalidPackageName = "InvalidPackageName", 4 | InvalidConfig = "InvalidConfig", 5 | InvalidSemanticVersion = "InvalidSemanticVersion", 6 | InvalidDependencyVersion = "InvalidDependencyVersion", 7 | FileSystemObjectNotFound = "FileSystemObjectNotFound", 8 | SubfolderIsEmptyString = "SubfolderIsEmptyString", 9 | DuplicateSubfolder = "DuplicateSubfolder", 10 | DuplicateResolvedPackageName = "DuplicateResolvedPackageName", 11 | NonExistentTemplate = "NonExistentTemplate", 12 | CircularTemplateDependency = "CircularTemplateDependency", 13 | CircularTypeScriptProjectReferenceDependency = "CircularTypeScriptProjectReferenceDependency", 14 | ExplicitNameInPackageJsonConfig = "ExplicitNameInPackageJsonConfig", 15 | DuplicateDependencyInPackageJsonConfig = "DuplicateDependencyInPackageJsonConfig", 16 | JsonParseError = "JsonParseError", 17 | UnexpectedRuntimeError = "UnexpectedRuntimeError", 18 | UnexpectedFilesystemObject = "UnexpectedFilesystemObject", 19 | ExplicitlySetNonOverridableValueInTSConfigJson = "ExplicitlySetNonOverridableValueInTSConfigJson", 20 | UnknownPackageDependency = "UnknownPackageDependency", 21 | NoLeafPackages = "NoLeafPackages", 22 | ToolFileIsNotInDevEnv = "ToolFileIsNotInDevEnv" // Used to detect whether tool should be reloaded upon change. TODO: remove once skoville is utilized. 23 | } 24 | 25 | export interface ConfigError { 26 | type: ErrorType; 27 | message: string; 28 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/io-ts/config-types.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { pipe } from 'fp-ts/lib/pipeable'; 3 | import * as either from 'fp-ts/lib/Either'; 4 | import * as taskEither from 'fp-ts/lib/TaskEither'; 5 | import * as option from 'fp-ts/lib/Option'; 6 | import { SUCCESS, Success, PACKAGES_DIRECTORY_NAME, PACKAGE_NAME_CONFIG_PATH_REQUIRED_SUFFIX, GLOBAL_SCOPE_NAME } from '../../constants'; 7 | import * as array from 'fp-ts/lib/Array'; 8 | import { ConfigError } from '../../errors'; 9 | import { taskEithercoalesceConfigErrors } from '../../../sync-logic/error-coalesce'; 10 | import { traversePackageTree, generateInitialContext } from '../../../sync-logic/traverse-package-tree'; 11 | import * as semver from 'semver'; 12 | import { customType } from './custom-type-helpers'; 13 | import { convertErorrs } from './convert-errors'; 14 | import { exactly } from './exactly'; 15 | import { colorize } from '../../../colorize-special-text'; 16 | import validateNpmPackageName = require('validate-npm-package-name'); 17 | import ansicolor from 'ansicolor'; 18 | import { nonEmptyArray } from 'io-ts-types'; 19 | 20 | export const SemanticVersion = customType( 21 | 'SemanticVersion', 22 | (input): input is string => typeof input === 'string' && semver.valid(input) === input 23 | ); 24 | 25 | export const NodeDependency = customType( 26 | 'TSMonorepoNodeDependency = string | [string, string]', 27 | (input): input is string | [string, string] => typeof input === 'string' || (Array.isArray(input) && input.length === 2) 28 | ); 29 | 30 | export const Scope = customType( 31 | `Valid NPM Scope | "${colorize.scope(GLOBAL_SCOPE_NAME)}" ${ansicolor.white("(latter is for packages without a scope)")}`, 32 | (input): input is string => { 33 | return ( 34 | typeof input === 'string' && 35 | (input === GLOBAL_SCOPE_NAME || validateNpmPackageName(`${input}/test`).validForNewPackages) 36 | ); 37 | } 38 | ) 39 | 40 | const NodeDependencies = t.array(NodeDependency); 41 | export const CompletePackageJson = t.type({ 42 | version: SemanticVersion, 43 | dependencies: NodeDependencies, 44 | devDependencies: NodeDependencies, 45 | peerDependencies: NodeDependencies, 46 | optionalDependencies: NodeDependencies 47 | }); 48 | export const PartialPackageJson = t.partial(CompletePackageJson.props, 'package.json'); 49 | export const PartialTSConfigJson = t.partial({ 50 | compilerOptions: t.any, 51 | include: t.array(t.string), 52 | exclude: t.array(t.string), 53 | files: t.array(t.string) 54 | }, 'tsconfig.json'); 55 | const JsonConfigs = t.partial({ 56 | "package.json": PartialPackageJson, 57 | "tsconfig.json": PartialTSConfigJson 58 | }, 'Record'); 59 | 60 | const typeNameJunctionConfig = "JunctionConfig"; 61 | const typeNamePackageConfig = "PackageConfig"; 62 | 63 | export const PackageConfig = exactly(t.intersection([ 64 | t.type({ 65 | extends: t.array(t.string) 66 | }), 67 | t.partial({ 68 | files: exactly(t.partial({ 69 | json: JsonConfigs, 70 | ignore: t.record(t.string, t.array(t.string)) 71 | })), 72 | bundle: exactly(t.intersection([ 73 | t.type({ 74 | target: t.keyof({ 75 | node: true, 76 | web: true 77 | }), 78 | autoRestart: t.boolean, 79 | hot: t.boolean, 80 | entries: nonEmptyArray(t.string) 81 | }), 82 | t.partial({ 83 | clientServerEndpoint: t.string, 84 | }) 85 | ])) 86 | }) 87 | ])); 88 | export const JunctionConfig = t.record( 89 | t.string, 90 | t.unknown, 91 | `${typeNameJunctionConfig}={[nameSegment: string]: ${typeNameJunctionConfig}|${typeNamePackageConfig}}`); 92 | 93 | export const TSMonorepoJson = exactly(t.intersection([ 94 | t.type({ 95 | version: SemanticVersion, 96 | ttypescript: t.boolean, 97 | port: t.Int 98 | }), 99 | t.partial({ 100 | templates: t.record(t.string, PackageConfig), 101 | //packages: t.UnknownRecord 102 | packages: t.record(Scope, JunctionConfig) 103 | }) 104 | ])); 105 | 106 | const packageConfigExplanation = `To configure a package (rather than a junction), ensure the name segment ends with "${PACKAGE_NAME_CONFIG_PATH_REQUIRED_SUFFIX}"`; 107 | 108 | export const validatePackageConfig = (configPath: string[]) => (input: unknown) => pipe( 109 | PackageConfig.decode(input), 110 | either.mapLeft(convertErorrs(configPath)) 111 | ); 112 | 113 | export const validateJunctionConfig = (configPath: string[]) => (input: unknown) => pipe( 114 | JunctionConfig.decode(input), 115 | either.mapLeft(convertErorrs(configPath, configPath[0] === PACKAGES_DIRECTORY_NAME && configPath.length > 2 ? packageConfigExplanation : undefined)) 116 | ); 117 | 118 | export type MonorepoConfig = t.TypeOf; 119 | 120 | export function validateTSMonoRepoJsonShape(input: Object): taskEither.TaskEither { 121 | return pipe( 122 | // First validate shape of the json. 123 | TSMonorepoJson.decode(input), 124 | either.mapLeft(convertErorrs([])), 125 | // Second, traverse the packages, and validate the shape of each recursively. 126 | taskEither.fromEither, 127 | taskEither.chain(validInput => pipe( 128 | option.fromNullable(validInput.packages), 129 | taskEither.fromOption(() => SUCCESS), 130 | taskEither.fold( 131 | taskEither.right, 132 | packages => pipe( 133 | Object.entries(packages), 134 | array.map(([scopeName, packageJunction]) => pipe( 135 | packageJunction, 136 | validateJunctionConfig([PACKAGES_DIRECTORY_NAME, scopeName]), 137 | taskEither.fromEither, 138 | taskEither.chain(validPackageJunction => traversePackageTree( 139 | validPackageJunction, 140 | generateInitialContext(scopeName), 141 | () => taskEither.right(SUCCESS), // The traverse is already validating. 142 | () => taskEither.right(SUCCESS) // The traverse is already validating. 143 | )) 144 | )), 145 | taskEithercoalesceConfigErrors 146 | ) 147 | ), 148 | taskEither.map(() => validInput) 149 | )) 150 | ) 151 | } 152 | either.chain -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/io-ts/convert-errors.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { ErrorType, ConfigError } from "../../errors"; 3 | import { colorize } from '../../../colorize-special-text'; 4 | import ansicolor = require('ansicolor'); 5 | import { CONFIG_FILE_NAME } from '../../constants'; 6 | import { constructPresentableConfigObjectPath } from '../../console-formatters/config-path'; 7 | import { Exactly } from './exactly'; 8 | 9 | export const convertErorrs = (pathPrefix: string[], additionalMessage?: string) => (errors: t.Errors): ConfigError[] => { 10 | return errors.map(error => ({ 11 | type: ErrorType.InvalidConfig, 12 | message: (() => { 13 | const lastContextEntry = error.context[error.context.length - 1]; 14 | var priorContextEntry: t.ContextEntry | undefined = undefined; 15 | const contextWithParentTags: {parentTag: string | undefined, entry: t.ContextEntry}[] = []; 16 | for (var i = 0; i < error.context.length; ++i) { 17 | contextWithParentTags[i] = { 18 | parentTag: (priorContextEntry?.type as any)?._tag, 19 | entry: error.context[i] 20 | }; 21 | priorContextEntry = error.context[i]; 22 | } 23 | 24 | const keyPath = constructPresentableConfigObjectPath([ 25 | ...pathPrefix, 26 | ...contextWithParentTags 27 | .filter(({parentTag}) => parentTag && !parentTag.includes('IntersectionType')) 28 | .map(contextEntry => contextEntry.entry.key) 29 | ]); 30 | 31 | const badValue = lastContextEntry.actual; 32 | const badValueString = 33 | typeof badValue === 'string' ? 34 | `string value "${colorize.badValue(badValue)}"` : 35 | Array.isArray(badValue) ? 36 | `array [${colorize.badValue(String(badValue))}]` : 37 | lastContextEntry.type instanceof Exactly ? 38 | `object with unrecognized key "${colorize.badValue(error.message as string)}"` : 39 | // else 40 | `value ${colorize.badValue(JSON.stringify(badValue))}`; 41 | return `\n ${ansicolor.magenta('subject:')} ${colorize.file(CONFIG_FILE_NAME)}${keyPath}${ 42 | "\n"} ${ansicolor.red('error:')} expected type ${colorize.type(lastContextEntry.type.name)}, but instead got ${badValueString}${additionalMessage ? "\n\n" + additionalMessage : ""}`; 43 | })() 44 | })); 45 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/io-ts/custom-type-helpers.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | 3 | export function customType(name: string, is: t.Is) { 4 | return new t.Type(name, is, function(input: unknown, context) { 5 | if (is(input)) { 6 | return t.success(input); 7 | } else { 8 | return t.failure(input, context); 9 | } 10 | }, validated => String(validated)); 11 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/io-ts/exactly.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/gcanti/io-ts/issues/322#issuecomment-584658211 2 | // This was written to support version 2.2.0 3 | import * as t from 'io-ts' 4 | import { either, Either, isRight, left, right, Right } from 'fp-ts/lib/Either' 5 | 6 | const getIsCodec = (tag: string) => (codec: t.Any): codec is T => (codec as any)._tag === tag 7 | const isInterfaceCodec = getIsCodec>('InterfaceType') 8 | const isPartialCodec = getIsCodec>('PartialType') 9 | 10 | const getProps = (codec: t.HasProps): t.Props => { 11 | switch (codec._tag) { 12 | case 'RefinementType': 13 | case 'ReadonlyType': 14 | return getProps(codec.type) 15 | case 'InterfaceType': 16 | case 'StrictType': 17 | case 'PartialType': 18 | return codec.props 19 | case 'IntersectionType': 20 | return codec.types.reduce((props, type) => Object.assign(props, getProps(type)), {}) 21 | } 22 | } 23 | 24 | const getNameFromProps = (props: t.Props): string => Object.keys(props) 25 | .map((k) => `${k}: ${props[k].name}`) 26 | .join(', ') 27 | 28 | const getPartialTypeName = (inner: string): string => `Partial<${inner}>` 29 | 30 | const getExcessTypeName = (codec: t.Any): string => { 31 | if (isInterfaceCodec(codec)) { 32 | return `{| ${getNameFromProps(codec.props)} |}` 33 | } if (isPartialCodec(codec)) { 34 | return getPartialTypeName(`{| ${getNameFromProps(codec.props)} |}`) 35 | } 36 | return `Exactly<${codec.name}>` 37 | } 38 | 39 | const stripKeys = (o: T, props: t.Props): Either, T> => { 40 | const keys = Object.getOwnPropertyNames(o) 41 | const propsKeys = Object.getOwnPropertyNames(props) 42 | 43 | propsKeys.forEach((pk) => { 44 | const index = keys.indexOf(pk) 45 | if (index !== -1) { 46 | keys.splice(index, 1) 47 | } 48 | }) 49 | 50 | return keys.length 51 | ? left(keys) 52 | : right(o) 53 | } 54 | 55 | export const exactly = (codec: C, name: string = getExcessTypeName(codec)): Exactly => { 56 | const props: t.Props = getProps(codec) 57 | return new Exactly( 58 | name, 59 | (u): u is C => isRight(stripKeys(u, props)) && codec.is(u), 60 | (u, c) => either.chain( 61 | t.UnknownRecord.validate(u, c), 62 | () => either.chain( 63 | codec.validate(u, c), 64 | (a) => either.mapLeft( 65 | stripKeys(a, props), 66 | (keys) => keys.map((k) => ({ 67 | value: a[k], 68 | context: c, 69 | message: k, 70 | })), 71 | ), 72 | ), 73 | ), 74 | (a) => codec.encode((stripKeys(a, props) as Right).right), 75 | codec, 76 | codec._tag 77 | ) 78 | } 79 | 80 | export class Exactly extends t.Type { 81 | public readonly _tag: string; 82 | public constructor( 83 | name: string, 84 | is: Exactly['is'], 85 | validate: Exactly['validate'], 86 | encode: Exactly['encode'], 87 | public readonly type: C, 88 | originalTag: string 89 | ) { 90 | super(name, is, validate, encode); 91 | this._tag = `Exactly<${originalTag}>` 92 | } 93 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/merged-config.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { CompletePackageJson } from './io-ts/config-types'; 3 | import { MandatoryTSConfigJsonValues } from '../../sync-logic/input-validation/validate-tsconfig.json'; 4 | 5 | export type MergedPackageJson = t.TypeOf & { 6 | name: string; 7 | } 8 | 9 | export interface MergedPackageConfig { 10 | files: { 11 | json: { 12 | "package.json": MergedPackageJson; 13 | "tsconfig.json": typeof MandatoryTSConfigJsonValues; 14 | }, 15 | ignore: { 16 | [fileName: string]: string[] 17 | } 18 | } 19 | // TODO: include space for skoville config. 20 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/monorepo-package.ts: -------------------------------------------------------------------------------- 1 | import { MergedPackageConfig } from "./merged-config"; 2 | 3 | export interface MonorepoPackage { 4 | relativePath: string; 5 | name: string; 6 | version: string; 7 | relationships: { 8 | dependsOn: Record; 9 | dependencyOf: Record; 10 | }; 11 | config: MergedPackageConfig; 12 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/package.d.ts/validate-npm-package-name.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'validate-npm-package-name' { 2 | namespace validate { 3 | export const scopedPackagePattern: RegExp; 4 | } 5 | interface result { 6 | validForNewPackages: boolean; 7 | validForOldPackages: boolean; 8 | warnings?: string[]; 9 | errors?: string[]; 10 | } 11 | function validate(name: string): result; 12 | export = validate; 13 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/types/traits.ts: -------------------------------------------------------------------------------- 1 | export interface Terminateable { 2 | terminate: () => Promise 3 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/common/util.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | 3 | export function md5Hash(input: string): string { 4 | return crypto.createHash("md5").update(input).digest("hex"); 5 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/file-system/object.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | export enum FileSystemObjectType { 4 | nothing = "nothing", 5 | file = "file", 6 | directory = "directory", 7 | symlink = "symlink" 8 | } 9 | 10 | export type FileSystemObjectDescriptor = { 11 | path: string; 12 | type: FileSystemObjectType.nothing | FileSystemObjectType.file | FileSystemObjectType.directory; 13 | } | { 14 | path: string; 15 | type: FileSystemObjectType.symlink; 16 | destination: FileSystemObjectDescriptor; 17 | traversedPath: string; 18 | traversedType: FileSystemObjectType.nothing | FileSystemObjectType.file | FileSystemObjectType.directory; 19 | } 20 | 21 | export async function getFileSystemObjectDescriptor(path: string): Promise { 22 | const stats = await (async function() { 23 | try { 24 | return await fs.promises.lstat(path); 25 | } catch(e) { 26 | return undefined; 27 | } 28 | })(); 29 | if (stats === undefined) { 30 | return { 31 | path, 32 | type: FileSystemObjectType.nothing 33 | }; 34 | } else { 35 | if(stats.isSymbolicLink()) { 36 | const destination = await getFileSystemObjectDescriptor(await fs.promises.readlink(path)); 37 | return { 38 | path, 39 | type: FileSystemObjectType.symlink, 40 | destination, 41 | traversedPath: destination.type === FileSystemObjectType.symlink ? destination.traversedPath : destination.path, 42 | traversedType: destination.type === FileSystemObjectType.symlink ? destination.traversedType : destination.type 43 | }; 44 | } else { 45 | return { 46 | path, 47 | type: stats.isDirectory() ? FileSystemObjectType.directory : FileSystemObjectType.file 48 | }; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/file-system/parse-json.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'comment-json'; 2 | import * as ansicolor from 'ansicolor'; 3 | import { CONFIG_FILE_NAME } from '../common/constants'; 4 | import { ConfigError, ErrorType } from '../common/errors'; 5 | import { colorize } from '../colorize-special-text'; 6 | import * as either from 'fp-ts/lib/Either'; 7 | 8 | export function parseJson(json: string): either.Either { 9 | try { 10 | return either.right(parse(json, undefined, true) as Object); 11 | } catch(e) { 12 | if (e.name === "SyntaxError") { 13 | e.message = `\n ${ansicolor.magenta('subject:')} ${ansicolor.green(CONFIG_FILE_NAME)}${ 14 | "\n" 15 | } ${ansicolor.red("error")}: ${e.message} on line ${ansicolor.green(e.line)}, column ${ansicolor.green(""+(e.column+1))}`; 16 | e.stack = undefined; 17 | } 18 | e.name = `${colorize.file(CONFIG_FILE_NAME)} parse error from library ${ansicolor.cyan("comment-json")}`; 19 | return either.left([{ 20 | type: ErrorType.JsonParseError, 21 | message: `${e.name}\n${e.message}` 22 | }]); 23 | } 24 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/file-system/presence-assertions.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { FileSystemObjectType, getFileSystemObjectDescriptor, FileSystemObjectDescriptor } from "./object"; 3 | import * as fs from 'fs'; 4 | import { ConfigError, ErrorType } from '../common/errors'; 5 | import { colorize } from '../colorize-special-text'; 6 | import { left, right } from 'fp-ts/lib/Either'; 7 | import { Success, SUCCESS } from '../common/constants'; 8 | import { TaskEither } from 'fp-ts/lib/TaskEither'; 9 | 10 | export function assertFileSystemObjectType(pathRelativeToProjectRoot: string, isOneOf: FileSystemObjectType[]): TaskEither { 11 | return async () => { 12 | const descriptor = await getFileSystemObjectDescriptor(path.resolve(pathRelativeToProjectRoot)); 13 | if (!isOneOf.includes(descriptor.type)) { 14 | return left([{ 15 | type: ErrorType.FileSystemObjectNotFound, 16 | message: `Expected a ${isOneOf.join(' or ')} at ${colorize.file(pathRelativeToProjectRoot)}. Found ${descriptor.type} instead.` 17 | }]); 18 | } 19 | return right(descriptor); 20 | }; 21 | } 22 | 23 | export function assertDirectoryExistsOrCreate(directoryPathRelativeToProjectRoot: string): TaskEither { 24 | return async () => { 25 | const absolutePath = path.resolve(directoryPathRelativeToProjectRoot); 26 | const descriptor = await getFileSystemObjectDescriptor(absolutePath); 27 | if (descriptor.type === FileSystemObjectType.nothing) { 28 | await fs.promises.mkdir(absolutePath); 29 | } else if (descriptor.type !== FileSystemObjectType.directory) { 30 | return left([ 31 | { 32 | type: ErrorType.FileSystemObjectNotFound, 33 | message: `Expected ${FileSystemObjectType.directory} at ${colorize.directory(directoryPathRelativeToProjectRoot)}. Found ${descriptor.type} instead.` 34 | } 35 | ]); 36 | } 37 | return right(SUCCESS); 38 | }; 39 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/file-system/watcher.ts: -------------------------------------------------------------------------------- 1 | import * as chokidar from 'chokidar'; 2 | import { FileSystemObjectDescriptor, getFileSystemObjectDescriptor } from './object'; 3 | import { Terminateable } from '../common/types/traits'; 4 | interface FileWatcherHandlers { 5 | onExists?: (descriptor: FileSystemObjectDescriptor) => void; 6 | onChange?: (descriptor: FileSystemObjectDescriptor) => void; 7 | onRemove?: (path: string, wasDirectory: boolean) => void; 8 | } 9 | 10 | export async function watch(path: string, handlers: FileWatcherHandlers): Promise { 11 | const watcher = chokidar.watch(path, {followSymlinks: false}); 12 | if (handlers.onExists) { 13 | const onExists = handlers.onExists; 14 | watcher 15 | .on("add", async (path, _stats) => { 16 | return onExists(await getFileSystemObjectDescriptor(path)) 17 | }) 18 | .on("addDir", async (path, _stats) => { 19 | return onExists(await getFileSystemObjectDescriptor(path)) 20 | }); 21 | } 22 | if (handlers.onChange) { 23 | const onChange = handlers.onChange; 24 | watcher 25 | .on("change", async (path, _stats) => { 26 | return onChange(await getFileSystemObjectDescriptor(path)) 27 | }); 28 | } 29 | if (handlers.onRemove) { 30 | const onRemove = handlers.onRemove; 31 | watcher 32 | .on("unlink", async path => { 33 | onRemove(path, false); 34 | }) 35 | .on("unlinkDir", async path => { 36 | onRemove(path, true); 37 | }); 38 | } 39 | watcher.on("error", error => { 40 | throw error; 41 | }); 42 | return new Promise(resolve => { 43 | watcher.on("ready", () => { 44 | resolve({ 45 | terminate: () => watcher.close() 46 | }); 47 | }); 48 | }); 49 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/logging/log.ts: -------------------------------------------------------------------------------- 1 | import { identity } from 'io-ts'; 2 | import * as ansicolor from 'ansicolor'; 3 | 4 | function getLogPrefix(colorTransformer: (logType: string) => string, logType: string) { 5 | return `[${ansicolor.dim("TSMR")}${logType.padStart(6).split(logType).join(colorTransformer(logType))}]`; 6 | } 7 | 8 | export const log = { 9 | error(...subjects: any[]) { 10 | console.log(getLogPrefix(ansicolor.lightRed, "ERROR"), ...subjects); 11 | }, 12 | warn(...subjects: any[]) { 13 | console.log(getLogPrefix(ansicolor.yellow, "WARN"), ...subjects); 14 | }, 15 | info(...subjects: any[]) { 16 | console.log(getLogPrefix(identity, "INFO"), ...subjects); 17 | }, 18 | trace(...subjects: any[]) { 19 | console.log(getLogPrefix(ansicolor.lightMagenta, "TRACE"), ...subjects); 20 | } 21 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/logging/pipe-debug-log.ts: -------------------------------------------------------------------------------- 1 | import * as either from 'fp-ts/lib/Either'; 2 | import * as taskEither from 'fp-ts/lib/TaskEither'; 3 | 4 | // These utility functions are for debugging. 5 | 6 | export function taskEitherLog(te: taskEither.TaskEither) { 7 | taskEither.fold( 8 | left => { 9 | console.log("TaskEither Left =", left); 10 | return async () => {}; 11 | }, 12 | right => { 13 | console.log("TaskEither Right =", right); 14 | return async () => {}; 15 | } 16 | )(te)(); 17 | return te; 18 | } 19 | 20 | export function eitherLog(e: either.Either) { 21 | either.fold( 22 | left => { 23 | console.log("Either Left =", left); 24 | }, 25 | right => { 26 | console.log("Either Right =", right); 27 | } 28 | )(e); 29 | return e; 30 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/logging/runtime-type-inference.ts: -------------------------------------------------------------------------------- 1 | export const inspectTypeAtRuntime = (object: any, name: string, prefix: string, remainingLevels: number) => { 2 | if (remainingLevels < 0) return; 3 | const typeOf = typeof object; 4 | console.log(`${prefix}type of ${name} is ${typeOf}`); 5 | if (typeOf === 'object') { 6 | const keys = Object.getOwnPropertyNames(object); 7 | console.log(`${prefix}keys of ${name} are ${keys}`); 8 | keys.forEach(key => { 9 | const val = object[key]; 10 | const typeofVal = typeof val; 11 | const keyName = `${name}.${key}` 12 | console.log(`${prefix}type of ${keyName} is ${typeofVal}`); 13 | if (typeofVal === 'object') { 14 | inspectTypeAtRuntime(val, keyName, prefix + '\t', remainingLevels - 1); 15 | } else if (typeofVal !== 'function') { 16 | console.log(`${prefix}value of ${keyName} is ${val}`); 17 | } 18 | }); 19 | } 20 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/package-dependency-logic/berry-install/install-with-berry.ts: -------------------------------------------------------------------------------- 1 | import * as taskEither from 'fp-ts/lib/TaskEither'; 2 | import * as either from 'fp-ts/lib/Either'; 3 | import { Success, SUCCESS } from '../../common/constants'; 4 | import { ConfigError } from '../../common/errors'; 5 | import { CommandRunner } from '../../process/command-runner'; 6 | import { pipe } from 'fp-ts/lib/pipeable'; 7 | 8 | export function installViaBerry(): taskEither.TaskEither { 9 | return pipe( 10 | async () => { 11 | await (new CommandRunner("yarn install")).waitUntilDone(); 12 | return either.right(SUCCESS); 13 | } 14 | ); 15 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/package-dependency-logic/monorepo-package-registry.ts: -------------------------------------------------------------------------------- 1 | import { MonorepoPackage } from "../common/types/monorepo-package"; 2 | import * as t from 'io-ts'; 3 | import { NodeDependency } from '../common/types/io-ts/config-types'; 4 | import { MergedPackageConfig } from "../common/types/merged-config"; 5 | import { ConfigError, ErrorType } from "../common/errors"; 6 | import { colorize } from "../colorize-special-text"; 7 | import * as either from 'fp-ts/lib/Either'; 8 | import * as semver from 'semver'; 9 | import * as option from 'fp-ts/lib/Option'; 10 | import { Success, SUCCESS } from "../common/constants"; 11 | import { pipe } from "fp-ts/lib/pipeable"; 12 | import * as array from 'fp-ts/lib/Array'; 13 | import { md5Hash } from "../common/util"; 14 | 15 | export class MonorepoPackageRegistry { 16 | private readonly packages: Map; 17 | private readonly leafSet: Set; 18 | private hashOfAllPackages: string; 19 | private timesRecalculatedHash = 0; 20 | 21 | public constructor() { 22 | this.packages = new Map(); 23 | this.leafSet = new Set(); 24 | this.hashOfAllPackages = ""; 25 | } 26 | 27 | public registerPackage(mergedPackageConfig: MergedPackageConfig, relativePath: string): either.Either { 28 | const mergedPackageJson = mergedPackageConfig.files.json["package.json"]; 29 | const packageName = mergedPackageJson.name; 30 | const existingPackageInRegistry = this.packages.get(packageName); 31 | if (existingPackageInRegistry !== undefined) { 32 | return either.left([{ 33 | type: ErrorType.DuplicateResolvedPackageName, 34 | message: `Attempt to register package ${colorize.package(packageName)} with version ${mergedPackageJson.version 35 | } which is already registered with version ${existingPackageInRegistry.version}.` 36 | }]); 37 | } 38 | const monorepoPackage: MonorepoPackage = { 39 | relativePath, 40 | name: mergedPackageJson.name, 41 | version: mergedPackageJson.version, 42 | relationships: { 43 | dependsOn: {}, 44 | dependencyOf: {} 45 | }, 46 | config: mergedPackageConfig 47 | }; 48 | this.packages.set(packageName, monorepoPackage); 49 | return either.right(SUCCESS); 50 | } 51 | 52 | public getLeafSet() { 53 | this.resolveMonorepoDependencies(); 54 | return new Set(this.leafSet); 55 | } 56 | 57 | public getRegisteredPackages() { 58 | this.resolveMonorepoDependencies(); 59 | return new Set(this.packages.values()); 60 | } 61 | public ensureNoCircularDependencies(): either.Either { 62 | this.resolveMonorepoDependencies(); 63 | // TODO: find way to support typescript project reference circular dependency since Node supports this. 64 | // We will use a memoized, recursive solution here. 65 | const knownPackagesWithCircularDependenciesOnThemSelves = new Map(); 66 | function recursivelyDetermineIfAPackageHasACircularDependencyOnItself([packageName, monorepoPackage]: [string, MonorepoPackage], dependencyChain: string[]) { 67 | if (knownPackagesWithCircularDependenciesOnThemSelves.has(packageName)) return; 68 | const newDependencyChain = [...dependencyChain, packageName]; 69 | if (dependencyChain.includes(packageName)) { 70 | const indexOfPackageName = dependencyChain.indexOf(packageName); 71 | knownPackagesWithCircularDependenciesOnThemSelves.set(packageName, newDependencyChain.slice(indexOfPackageName)); 72 | } else { 73 | Object.entries(monorepoPackage.relationships.dependsOn).forEach(dependencyEntry => 74 | recursivelyDetermineIfAPackageHasACircularDependencyOnItself(dependencyEntry, newDependencyChain)); 75 | } 76 | } 77 | Array.from(this.packages.entries()) 78 | .forEach(packageEntry => recursivelyDetermineIfAPackageHasACircularDependencyOnItself(packageEntry, [])); 79 | if (knownPackagesWithCircularDependenciesOnThemSelves.size > 0) { 80 | return pipe( 81 | Array.from(knownPackagesWithCircularDependenciesOnThemSelves.entries()), 82 | array.map(([packageName, dependencyChain]) => ({ 83 | type: ErrorType.CircularTypeScriptProjectReferenceDependency, 84 | message: `The package ${colorize.package(packageName)} has a circular typescript project references dependency on itself via relationship graph${ 85 | "\n"}${dependencyChain.map(colorize.package).join(" depends on\n")}` 86 | })), 87 | either.left 88 | ); 89 | } else { 90 | return either.right(SUCCESS); 91 | } 92 | } 93 | 94 | /** 95 | * Returns whether any changes were made to the internal dependency graph. 96 | */ 97 | private resolveMonorepoDependencies(): boolean { 98 | // TODO: the below 2 lines of code could be considered premature optimization. Run some heuristics to see what is preferable. 99 | const currentHashOfPackages = this.calculatePackagesHash(); 100 | if (currentHashOfPackages === this.hashOfAllPackages) return false; 101 | if (this.timesRecalculatedHash > 0) { 102 | throw new Error(`Logical error: recalculated hash ${this.timesRecalculatedHash} times.`); 103 | } 104 | this.timesRecalculatedHash++; 105 | // Clear all known information on dependencies. 106 | for (const registeredPackage of this.packages.values()) { 107 | registeredPackage.relationships.dependsOn = {}; 108 | registeredPackage.relationships.dependencyOf = {}; 109 | } 110 | // Loop through each package of the registry. Then loop through each of the dependencies of each package. 111 | // For each of a package's dependencies, if the dependency exists in registry, inspect, otherwise ignore. 112 | // For every dependency found in registry, update its "dependencyOf", and update package's "dependsOn". 113 | for (const registeredPackage of this.packages.values()) { 114 | const mergedPackageJson = registeredPackage.config.files.json["package.json"]; 115 | const definiteDependencies: t.TypeOf[] = [ 116 | ...mergedPackageJson.dependencies, 117 | ...mergedPackageJson.devDependencies, 118 | ...mergedPackageJson.optionalDependencies, 119 | ...mergedPackageJson.peerDependencies 120 | ]; 121 | const seenDefiniteDependencies = new Set(); 122 | for (const dependency of definiteDependencies) { 123 | // The problem here is that we need to define the behavior of a NodeDependency which does 124 | // not give an exact version versus when it does, and in both scenarios whether this 125 | // will result in an ignore when searching through the registry. 126 | // 1. If it is just a blank version string instead of [string, string], 127 | // then we need to assume the user is choosing either the latest version or referencing a monorepo package 128 | // If the dependencyName is in the registry, we assume monorepo reference, otherwise we assume latest version 129 | // 2. If it is not a blank version [string, string], then we assume user wants to reference version not managed by monorepo, and 130 | // thus we can use pacote to obtain the version they seek. 131 | const [dependencyName] = 132 | Array.isArray(dependency) ? dependency: 133 | [dependency]; 134 | if (seenDefiniteDependencies.has(dependencyName)) { 135 | continue; 136 | } else { 137 | seenDefiniteDependencies.add(dependencyName); 138 | } 139 | const maybeRegisteredDepdendency = this.getMonorepoPackageIfCompatibleAndPresent(dependency); 140 | if (option.isSome(maybeRegisteredDepdendency)) { 141 | const registeredDependency = maybeRegisteredDepdendency.value; 142 | registeredDependency.relationships.dependencyOf[registeredPackage.name] = registeredPackage; 143 | registeredPackage.relationships.dependsOn[registeredDependency.name] = registeredDependency; 144 | } 145 | } 146 | } 147 | this.hashOfAllPackages = this.calculatePackagesHash(); 148 | this.leafSet.clear(); 149 | // Loop through each package of the registry. If its "dependencyOf" has no elements, 150 | // then it is a leaf package. 151 | for (const registeredPackage of this.packages.values()) { 152 | if (Object.keys(registeredPackage.relationships.dependencyOf).length === 0) { 153 | this.leafSet.add(registeredPackage); 154 | } 155 | } 156 | return true; 157 | } 158 | 159 | private calculatePackagesHash() { 160 | return pipe( 161 | Array.from(this.packages.entries()), 162 | array.map(([packageName, monorepoPackage]) => [ 163 | packageName, 164 | Object.keys(monorepoPackage.relationships.dependsOn) 165 | ]), 166 | Object.fromEntries, 167 | JSON.stringify, 168 | md5Hash 169 | ); 170 | } 171 | 172 | /** 173 | * @returns monorepo package from registry IFF the following conditions are met: 174 | * 1. Package registry contains a package with given name 175 | * 2. Provided version value is undefined or version of package in registry "satisfies" provided defined version value according to semver. 176 | */ 177 | public getMonorepoPackageIfCompatibleAndPresent(reference: t.TypeOf): option.Option { 178 | const [packageName, packageVersion] = 179 | Array.isArray(reference) ? reference : 180 | [reference, undefined]; // TODO: let's change this by using Either instead. 181 | const registeredPackage = this.packages.get(packageName); 182 | if (registeredPackage === undefined) { 183 | return option.none; 184 | } else { 185 | if (packageVersion === undefined) { 186 | return option.some(registeredPackage); 187 | } else { 188 | if (semver.satisfies(registeredPackage.version, packageVersion)) { 189 | return option.some(registeredPackage); 190 | } else { 191 | return option.none; 192 | } 193 | } 194 | } 195 | } 196 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/process/command-runner.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import { log } from '../logging/log'; 3 | import { colorize } from '../colorize-special-text'; 4 | 5 | export class CommandRunner { 6 | private static readonly OS_IS_WINDOWS = require("os").platform() === 'win32'; 7 | private readonly commandProcess: child_process.ChildProcess; 8 | private readonly finishedPromise: Promise; 9 | private killed = false; 10 | 11 | public constructor(private readonly command: string) { 12 | const [executableName, ...commandArgs] = this.command.split(" "); 13 | this.commandProcess = child_process.spawn( 14 | `${executableName}${CommandRunner.OS_IS_WINDOWS ? '.cmd' : ''}`, 15 | commandArgs, {stdio: 'inherit', cwd: process.cwd() }); 16 | if (this.commandProcess.pid === undefined) { 17 | log.error(`Unable to start command ${colorize.command(this.command)}`); 18 | } else { 19 | log.info(`Command started on PID ${this.commandProcess.pid}: ${colorize.command(this.command)}`); 20 | } 21 | this.finishedPromise = new Promise(resolve => { 22 | this.commandProcess 23 | .on("error", err => { 24 | log.error("Received Error from process:"); 25 | log.error(err.stack || err.message); 26 | resolve(); 27 | }) 28 | .on("exit", (code, signal) => { 29 | const codeMessage = code ? " with code " + code : ""; 30 | const signalMessage = signal ? " via " + signal : ""; 31 | const doneVerb = (codeMessage.length === 0 && signalMessage.length === 0) ? "finished" : "terminated"; 32 | 33 | log.info(`Command ${doneVerb}${codeMessage}${signalMessage}: ${colorize.command(this.command)}`); 34 | this.killed = true; 35 | resolve(); 36 | }); 37 | }); 38 | } 39 | 40 | public async waitUntilDone() { 41 | await this.finishedPromise; 42 | } 43 | 44 | public async kill() { 45 | if (this.killed) { 46 | return; // idempotent 47 | } 48 | if(CommandRunner.OS_IS_WINDOWS) { 49 | child_process.execSync("taskkill /F /T /pid " + this.commandProcess.pid); 50 | } else { 51 | this.commandProcess.kill("SIGTERM"); 52 | this.commandProcess.kill("SIGINT"); 53 | this.commandProcess.kill("SIGKILL"); 54 | this.commandProcess.kill("SIGQUIT"); 55 | } 56 | this.killed = true; 57 | await this.waitUntilDone(); 58 | } 59 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/process/parent-child-rpc.ts: -------------------------------------------------------------------------------- 1 | export interface ChildToParentMessage { 2 | type: "restart" 3 | } 4 | 5 | export interface ParentToChildMessage { 6 | type: "die" 7 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/process/restart-program.ts: -------------------------------------------------------------------------------- 1 | import { log } from '../logging/log'; 2 | import { ChildToParentMessage } from './parent-child-rpc'; 3 | import { parentPort } from 'worker_threads'; 4 | 5 | var restarting = false; 6 | export async function restartProgram(idempotentPreRestartFn: () => Promise | undefined) { 7 | if (restarting) return; 8 | restarting = true; 9 | if (idempotentPreRestartFn) await idempotentPreRestartFn(); 10 | log.trace("Restarting..."); 11 | const toSend: ChildToParentMessage = { type: "restart" }; 12 | parentPort!.postMessage(JSON.stringify(toSend)); 13 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/process/typescript-runner.ts: -------------------------------------------------------------------------------- 1 | import { Terminateable } from "../common/types/traits"; 2 | import { TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_RELATIVE_PATH } from "../common/constants"; 3 | import { MonorepoConfig } from "../common/types/io-ts/config-types"; 4 | import { CommandRunner } from "./command-runner"; 5 | 6 | function generateTSBuildCommand(ttypescipt: boolean) { 7 | return `${ttypescipt ? "t" : ""}tsc -b --watch --preserveWatchOutput ${TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_RELATIVE_PATH}`; 8 | } 9 | 10 | export const startTypeScript = (monorepoConfig: MonorepoConfig): Terminateable => { 11 | const buildTask = new CommandRunner(generateTSBuildCommand(monorepoConfig.ttypescript)); 12 | return { 13 | terminate: async () => { 14 | await buildTask.kill(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/self-change-detector.ts: -------------------------------------------------------------------------------- 1 | import { assertFileSystemObjectType } from "./file-system/presence-assertions"; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { PACKAGE_JSON_FILENAME, TS_BUILD_INFO_FILENAME, SUCCESS, TOOL_FULL_NAME, Success, TOOL_SHORT_NAME } from "./common/constants"; 5 | import { FileSystemObjectType } from "./file-system/object"; 6 | import { pipe } from 'fp-ts/lib/function'; 7 | import * as taskEither from 'fp-ts/lib/TaskEither'; 8 | import * as either from 'fp-ts/lib/Either'; 9 | import { ConfigError, ErrorType } from "./common/errors"; 10 | import { log } from "./logging/log"; 11 | import { colorize } from "./colorize-special-text"; 12 | import { md5Hash } from "./common/util"; 13 | import * as Option from 'fp-ts/lib/Option'; 14 | 15 | // This is some logic to determine if we are in the context of the project being developed, or if we are distributed as a dependency. 16 | const absolutePathOfTargetRepo = path.resolve('./'); 17 | const absolutePathOfPotentialProject = path.resolve(__filename, '../../'); 18 | const absolutePathOfPotentialProjectPackageJson = path.resolve(absolutePathOfPotentialProject, PACKAGE_JSON_FILENAME); 19 | const absolutePathOfPotentialProjectBuildInfo = path.resolve(absolutePathOfPotentialProject, TS_BUILD_INFO_FILENAME); 20 | 21 | const relativePathToPotentialProjectPackageJson = path.relative(absolutePathOfTargetRepo, absolutePathOfPotentialProjectPackageJson); 22 | const relativePathToPotentialProjectBuildInfo = path.relative(absolutePathOfTargetRepo, absolutePathOfPotentialProjectBuildInfo); 23 | 24 | const PROJECT_IS_NOT_IN_DEV_ENV: ConfigError[] = [{ 25 | type: ErrorType.ToolFileIsNotInDevEnv, 26 | message: '' 27 | }]; 28 | 29 | const STANDARD_ANOMALY_PREFIX = `Did detect change in ${colorize.file(TOOL_SHORT_NAME)}, but`; 30 | 31 | interface BuildInfo { 32 | fileNames: string[]; 33 | fileInfos: { 34 | version: string; 35 | signature: string; 36 | }[]; 37 | } 38 | 39 | function getBuildInfo() { 40 | return pipe( 41 | assertFileSystemObjectType(relativePathToPotentialProjectPackageJson, [FileSystemObjectType.file]), 42 | taskEither.chain(projectPackageJsonDescriptor => async (): Promise> => { 43 | const projectPackageJsonContents = (await fs.promises.readFile(projectPackageJsonDescriptor.path)).toString(); 44 | let projectPackageJson; 45 | try { 46 | projectPackageJson = JSON.parse(projectPackageJsonContents); 47 | } catch(e) { 48 | log.warn(`${STANDARD_ANOMALY_PREFIX} was unable to parse json at ${colorize.file(relativePathToPotentialProjectPackageJson)}.`); 49 | return either.left(PROJECT_IS_NOT_IN_DEV_ENV); 50 | } 51 | if (projectPackageJson.name !== TOOL_FULL_NAME) return either.left(PROJECT_IS_NOT_IN_DEV_ENV); 52 | return either.right(SUCCESS); 53 | }), 54 | taskEither.chain(() => assertFileSystemObjectType(relativePathToPotentialProjectBuildInfo, [FileSystemObjectType.file])), 55 | taskEither.chain(projectBuildInfoDescriptor => async (): Promise>> => { 56 | const projectBuildInfoJsonContents = (await fs.promises.readFile(projectBuildInfoDescriptor.path)).toString(); 57 | let projectBuildInfoJson; 58 | try { 59 | projectBuildInfoJson = JSON.parse(projectBuildInfoJsonContents); 60 | } catch(e) { 61 | log.warn(`${STANDARD_ANOMALY_PREFIX} was unable to parse json at ${colorize.file(relativePathToPotentialProjectBuildInfo)}`); 62 | // In this case since the package json is present so we can conclude that we are in a dev env, 63 | // but when the ts build info is malformed, we should panic (something's very wrong here) 64 | return either.right(Option.none); 65 | } 66 | const buildinfoProgram: BuildInfo = projectBuildInfoJson.program; 67 | return either.right(Option.some(buildinfoProgram)); 68 | }) 69 | ) 70 | } 71 | 72 | const currentBuildInfoPromise: Promise> = pipe( 73 | getBuildInfo(), 74 | taskEither.fold( 75 | _errors => async () => Option.none, 76 | maybeFileInfo => async () => maybeFileInfo 77 | ) 78 | )(); 79 | 80 | export async function initialize() { 81 | const maybeBuildInfo = await currentBuildInfoPromise; 82 | if (Option.isSome(maybeBuildInfo)) { 83 | log.info(`${colorize.subfeature("build:rapid")} mode enabled`); 84 | } 85 | } 86 | 87 | interface ProgramChanges { 88 | filesAdded: string[]; 89 | filesRemoved: string[]; 90 | filesChanged: string[]; 91 | filesWithSignatureChanges: string[]; 92 | } 93 | 94 | const CONFIRMED_CHANGE_BUT_WITH_UNKNOWN_SPECIFICS: ProgramChanges = { 95 | filesAdded: [], 96 | filesRemoved: [], 97 | filesChanged: [], 98 | filesWithSignatureChanges: [] 99 | }; 100 | 101 | function calculateChanges(oldBuildInfo: BuildInfo, newBuildInfo: BuildInfo): Option.Option { 102 | const changes: string[] = []; 103 | const signatureChanges: string[] = []; 104 | 105 | const oldHash = md5Hash(JSON.stringify(oldBuildInfo)); 106 | const newHash = md5Hash(JSON.stringify(newBuildInfo)); 107 | if (oldHash === newHash) { 108 | log.trace(`${colorize.subfeature("build:rapid")} mode: current ${colorize.package(TOOL_SHORT_NAME)} tool hash is ${oldHash}`); 109 | } else { 110 | log.trace(`${colorize.subfeature("build:rapid")} mode: change detected; old ${colorize.package(TOOL_SHORT_NAME)} tool hash is ${oldHash}`); 111 | log.trace(`${colorize.subfeature("build:rapid")} mode: change detected; new ${colorize.package(TOOL_SHORT_NAME)} tool hash is ${newHash}`); 112 | } 113 | 114 | // files added 115 | const added: string[] = []; 116 | newBuildInfo.fileInfos.forEach((_fileInfo, idx) => { 117 | if (oldBuildInfo.fileInfos[idx] === undefined) { 118 | added.push(newBuildInfo.fileNames[idx]); 119 | } else { 120 | const oldFile = oldBuildInfo.fileInfos[idx]; 121 | const newFile = newBuildInfo.fileInfos[idx]; 122 | // Keys where version changed 123 | if (oldFile.version !== newFile.version) { 124 | changes.push(newBuildInfo.fileNames[idx]); 125 | } 126 | // Keys where signature changed. 127 | if (oldFile.signature !== newFile.signature) { 128 | signatureChanges.push(newBuildInfo.fileNames[idx]); 129 | } 130 | } 131 | }) 132 | 133 | // files deleted 134 | const removed: string[] = []; 135 | oldBuildInfo.fileInfos.forEach((_fileInfo, idx) => { 136 | if (newBuildInfo.fileInfos[idx] === undefined) { 137 | removed.push(oldBuildInfo.fileNames[idx]); 138 | } 139 | }) 140 | 141 | const actualChangesOccurred = changes.length > 0 || signatureChanges.length > 0 || added.length > 0 || removed.length > 0; 142 | if (!actualChangesOccurred) return Option.none; 143 | return Option.some({ 144 | filesAdded: added, 145 | filesRemoved: removed, 146 | filesChanged: changes, 147 | filesWithSignatureChanges: signatureChanges 148 | }); 149 | } 150 | 151 | // TODO: we should also compare the package.json from before and after 152 | export const detectProgramChanges = async (): Promise> => { 153 | const currentBuildInfo = await currentBuildInfoPromise; 154 | 155 | if (Option.isNone(currentBuildInfo)) { 156 | log.warn("No original build info file loaded. Assuming that change occurred"); 157 | return Option.some(CONFIRMED_CHANGE_BUT_WITH_UNKNOWN_SPECIFICS); 158 | } 159 | 160 | return pipe( 161 | getBuildInfo(), 162 | taskEither.chain(newBuildInfo => async (): Promise>> => { 163 | if (Option.isNone(newBuildInfo)) { 164 | log.warn("No new build info file loaded. Assuming that change occurred"); 165 | return either.right(Option.some(CONFIRMED_CHANGE_BUT_WITH_UNKNOWN_SPECIFICS)); 166 | } 167 | return either.right(calculateChanges(currentBuildInfo.value, newBuildInfo.value)); 168 | }), 169 | taskEither.fold( 170 | // If any errors happened, we conclude that this could not have been the dev env, 171 | // so we therefore consider the change event (which provoked this method to be called) is valid. 172 | // We don't have any information on ts-build info since we aren't in dev env, so we give empty arrays for changes. 173 | _errors => async () => { 174 | return Option.some(CONFIRMED_CHANGE_BUT_WITH_UNKNOWN_SPECIFICS) 175 | }, 176 | 177 | maybeProgramChanges => async () => maybeProgramChanges 178 | ) 179 | )(); 180 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/cached-latest-version-fetcher.ts: -------------------------------------------------------------------------------- 1 | import * as latestVersion from 'latest-version'; 2 | export class CachedLatestVersionFetcher { 3 | private readonly latestVersionsMap: Map>; 4 | public constructor() { 5 | this.latestVersionsMap = new Map(); 6 | } 7 | public latestVersion(dependency: string): Promise { 8 | const latestVersionPromise = this.latestVersionsMap.get(dependency); 9 | if (latestVersionPromise === undefined) { 10 | const newLatestVersionPromise = latestVersion(dependency); 11 | this.latestVersionsMap.set(dependency, newLatestVersionPromise); 12 | return newLatestVersionPromise; 13 | } else { 14 | return latestVersionPromise; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/input-to-merged/files/package.json.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { PartialPackageJson } from '../../../../common/types/io-ts/config-types'; 3 | import { MergedPackageJson } from "../../../../common/types/merged-config"; 4 | 5 | export function convertToMergedPackageJSON(name: string, inputPackageJSON?: t.TypeOf): MergedPackageJson { 6 | return { 7 | ...inputPackageJSON, // Their own input. 8 | name, 9 | dependencies: inputPackageJSON?.dependencies ?? [], 10 | devDependencies: inputPackageJSON?.devDependencies ?? [], 11 | peerDependencies: inputPackageJSON?.peerDependencies ?? [], 12 | optionalDependencies: inputPackageJSON?.optionalDependencies ?? [] 13 | } as MergedPackageJson; 14 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/input-to-merged/package-config.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { MergedPackageConfig } from "../../../common/types/merged-config"; 3 | import { convertToMergedPackageJSON } from "./files/package.json"; 4 | import deepmerge = require("deepmerge"); 5 | import { colorize } from "../../../colorize-special-text"; 6 | import { PackageConfig } from '../../../common/types/io-ts/config-types'; 7 | import { TS_CONFIG_JSON_FILENAME, PACKAGE_JSON_FILENAME } from '../../../common/constants'; 8 | import { MandatoryTSConfigJsonValues } from '../../input-validation/validate-tsconfig.json'; 9 | import { MANDATORY_PACKAGE_JSON_VALUES } from '../../input-validation/validate-package.json'; 10 | 11 | export function mergePackageConfig(templates: Map, 12 | subject: t.TypeOf, name: string = "", version?: string): MergedPackageConfig { 13 | 14 | var result: MergedPackageConfig 15 | // 1. Merge default settings into result 16 | = version ? { 17 | files: { 18 | json: { 19 | [PACKAGE_JSON_FILENAME]: { 20 | version 21 | } 22 | } 23 | } 24 | } as MergedPackageConfig : 25 | {} as MergedPackageConfig; 26 | 27 | // 2. Merge templates into result. 28 | const templatesToMerge = [...subject.extends]; 29 | for (var templateName = templatesToMerge.shift(); templateName !== undefined; templateName = templatesToMerge.shift()) { 30 | const currentTemplate = templates.get(templateName); 31 | if (currentTemplate === undefined) { 32 | throw new Error(`Template to merge ${colorize.template(templateName)} is unknown`); 33 | } 34 | result = deepmerge(result, currentTemplate); 35 | } 36 | 37 | // 3. Merge subject into result. 38 | const packageJSON = (subject.files && subject.files.json) ? subject.files.json[PACKAGE_JSON_FILENAME] : undefined; 39 | const packageConfigMerged = { 40 | files: { 41 | ...(subject.files ?? {}), 42 | json: { 43 | ...(subject?.files?.json ?? {}), 44 | [PACKAGE_JSON_FILENAME]: convertToMergedPackageJSON(name, packageJSON) 45 | }, 46 | ignore: { 47 | ...(subject?.files?.ignore ?? {}) 48 | } 49 | } 50 | } as MergedPackageConfig; 51 | result = deepmerge(result, packageConfigMerged); 52 | 53 | // 4. Merge non-overridable settings into result 54 | result = deepmerge(result, { 55 | files: { 56 | json: { 57 | [PACKAGE_JSON_FILENAME]: MANDATORY_PACKAGE_JSON_VALUES, 58 | [TS_CONFIG_JSON_FILENAME]: MandatoryTSConfigJsonValues 59 | } 60 | } 61 | }); 62 | 63 | return result; 64 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/monorepo-to-output/files/monorepo-package.json.ts: -------------------------------------------------------------------------------- 1 | import { MonorepoPackageRegistry } from "../../../../package-dependency-logic/monorepo-package-registry"; 2 | import * as taskEither from 'fp-ts/lib/TaskEither'; 3 | import * as either from 'fp-ts/lib/Either'; 4 | import { ConfigError } from "../../../../common/errors"; 5 | import { pipe } from 'fp-ts/lib/function'; 6 | import { assertFileSystemObjectType } from "../../../../file-system/presence-assertions"; 7 | import { MONOREPO_PACKAGE_JSON_RELATIVE_PATH, MONOREPO_PACKAGE_JSON_ABSOLUTE_PATH } from "../../../../common/constants"; 8 | import { FileSystemObjectType } from "../../../../file-system/object"; 9 | import * as fs from 'fs'; 10 | 11 | export function monorepoPackageRegistryToMonorepoRootPackageJson(monorepoPackageRegistry: MonorepoPackageRegistry): taskEither.TaskEither { 12 | return pipe( 13 | assertFileSystemObjectType(MONOREPO_PACKAGE_JSON_RELATIVE_PATH, [FileSystemObjectType.file, FileSystemObjectType.nothing]), 14 | taskEither.chain(descriptor => async (): Promise> => { 15 | const valuesToOverwrite: Object = { 16 | private: true, 17 | workspaces: Array.from(monorepoPackageRegistry.getRegisteredPackages().values()).map(registeredPackage => { 18 | // If we don't use posix sep ('/' instead of windows '\'), then yarn + tsc can run into trouble resolving dependencies 19 | return registeredPackage.relativePath 20 | // replaceAll not supported by node 14 so using .split.join 21 | .split('\\').join('/'); 22 | }) 23 | }; 24 | 25 | if (descriptor.type === FileSystemObjectType.file) { 26 | const existingFileContents = (await (fs.promises.readFile(MONOREPO_PACKAGE_JSON_ABSOLUTE_PATH))).toString(); 27 | var existingFileJSON: Object; 28 | try { 29 | existingFileJSON = JSON.parse(existingFileContents); 30 | } catch(e) { 31 | return either.right(valuesToOverwrite); 32 | } 33 | 34 | Object.keys(valuesToOverwrite).forEach(key => { 35 | (existingFileJSON as any)[key] = (valuesToOverwrite as any)[key]; 36 | }) 37 | 38 | return either.right(existingFileJSON); 39 | } else { 40 | return either.right(valuesToOverwrite); 41 | } 42 | }) 43 | ); 44 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/monorepo-to-output/files/package.json.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import * as taskEither from 'fp-ts/lib/TaskEither'; 3 | import * as either from 'fp-ts/lib/Either'; 4 | import * as array from 'fp-ts/lib/Array'; 5 | import { MonorepoPackage } from "../../../../common/types/monorepo-package"; 6 | import { MonorepoPackageRegistry } from "../../../../package-dependency-logic/monorepo-package-registry"; 7 | import { PACKAGE_JSON_FILENAME } from "../../../../common/constants"; 8 | import { CachedLatestVersionFetcher } from "../../../cached-latest-version-fetcher"; 9 | import { NodeDependency } from '../../../../common/types/io-ts/config-types'; 10 | import { ConfigError, ErrorType } from '../../../../common/errors'; 11 | import { pipe } from 'fp-ts/lib/pipeable'; 12 | import { taskEitherCoalesceConfigErrorsAndObject } from '../../../error-coalesce'; 13 | import { colorize } from '../../../../colorize-special-text'; 14 | import * as option from 'fp-ts/lib/Option'; 15 | 16 | type DependencyType = ('dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies'); 17 | 18 | const getLatestVersionedDependencyEntry = (targetPackageName: string, dependencyType: DependencyType, dependencyName: string, latestVersionGetter: CachedLatestVersionFetcher) 19 | : taskEither.TaskEither => async () => { 20 | try { 21 | const latestVersion = await latestVersionGetter.latestVersion(dependencyName); 22 | return either.right([dependencyName, `^${latestVersion}`]); 23 | } catch (e) { 24 | if (e.name === 'PackageNotFoundError') { 25 | return either.left([{ 26 | type: ErrorType.UnknownPackageDependency, 27 | message: `${colorize.package(targetPackageName)} delcares ${colorize.package(dependencyName)} a member of ${colorize.file(PACKAGE_JSON_FILENAME)}[${dependencyType}].${ 28 | "\n"}However ${colorize.package(dependencyName)} is not registered in npm nor configured in the monorepo.` 29 | }]) 30 | } 31 | throw e; 32 | } 33 | } 34 | 35 | function convertDependencies( 36 | targetPackageName: string, 37 | dependencyType: DependencyType, 38 | dependencies: t.TypeOf[], 39 | monorepoPackageRegistry: MonorepoPackageRegistry, 40 | latestVersionGetter: CachedLatestVersionFetcher): taskEither.TaskEither> { 41 | 42 | return pipe( 43 | dependencies, 44 | array.map(dependency => { 45 | const [dependencyName, dependencyVersion] = Array.isArray(dependency) ? dependency : [dependency, undefined]; 46 | const maybeDependentMonorepoPackage = monorepoPackageRegistry.getMonorepoPackageIfCompatibleAndPresent(dependency); 47 | if (option.isSome(maybeDependentMonorepoPackage)) { 48 | if (dependencyVersion === undefined) { 49 | return taskEither.right([dependencyName, maybeDependentMonorepoPackage.value.version]); 50 | } else { 51 | return taskEither.right([dependencyName, dependencyVersion]); 52 | } 53 | } else { 54 | if (dependencyVersion === undefined) { 55 | return getLatestVersionedDependencyEntry(targetPackageName, dependencyType, dependencyName, latestVersionGetter); 56 | } else { 57 | return taskEither.right([dependencyName, dependencyVersion]); 58 | } 59 | } 60 | }), 61 | taskEitherCoalesceConfigErrorsAndObject, 62 | taskEither.map(Object.fromEntries) 63 | ); 64 | } 65 | 66 | const dependencyKeys: DependencyType[] = [ 67 | 'dependencies', 68 | 'devDependencies', 69 | 'peerDependencies', 70 | 'optionalDependencies' 71 | ]; 72 | 73 | // Yarn will remove empty deps objects from package.json, so the below line will prevent "adding [peerDependencies]: {...}" 74 | // messages every time the ts-monorepo.json is saved. 75 | function removeAllBlankDependencies(properPackageJsonObject: Object): Object { 76 | pipe( 77 | dependencyKeys, 78 | array.filter(key => Object.keys((properPackageJsonObject as any)[key]).length === 0), 79 | array.map(key => { 80 | delete (properPackageJsonObject as any)[key]; 81 | }) 82 | ); 83 | return properPackageJsonObject; 84 | } 85 | 86 | export function monorepoPackageToPackageJsonOutput( 87 | monorepoPackage: MonorepoPackage, 88 | monorepoPackageRegistry: MonorepoPackageRegistry, 89 | latestVersionGetter: CachedLatestVersionFetcher): taskEither.TaskEither { 90 | const packageJsonConfig = monorepoPackage.config.files.json[PACKAGE_JSON_FILENAME]; 91 | // TODO: typescript version should be validated coming in. It should only be allowed to be equal to the primary typescript version? Maybe 92 | 93 | return pipe( 94 | dependencyKeys, 95 | array.map(key => pipe( 96 | convertDependencies(monorepoPackage.name, key, packageJsonConfig[key], monorepoPackageRegistry, latestVersionGetter), 97 | taskEither.map(convertedDependencies => [key, convertedDependencies]) 98 | )), 99 | taskEitherCoalesceConfigErrorsAndObject, 100 | taskEither.map(Object.fromEntries), 101 | taskEither.map(allConvertedDependencies => Object.assign({}, packageJsonConfig, allConvertedDependencies)), 102 | taskEither.map(removeAllBlankDependencies) 103 | ); 104 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/monorepo-to-output/files/ts-project-leaves.json.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { MonorepoPackageRegistry } from "../../../../package-dependency-logic/monorepo-package-registry"; 3 | import { TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_ABSOLUTE_PATH } from '../../../../common/constants'; 4 | 5 | // TODO: how can we handle circular references? Should we before the following is solved? https://github.com/microsoft/TypeScript/issues/33685 6 | export function monorepoPackageRegistryToTSProjectLeavesJsonOutput(monorepoPackageRegistry: MonorepoPackageRegistry): Object { 7 | return { 8 | files: [], 9 | references: Array.from(monorepoPackageRegistry.getLeafSet()) 10 | .map(monorepoPackage => ({ 11 | path: path.relative( 12 | path.resolve(TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_ABSOLUTE_PATH, '../'), 13 | path.resolve(monorepoPackage.relativePath) 14 | ) 15 | // If we don't use posix sep ('/' instead of windows '\'), then yarn + tsc can run into trouble resolving dependencies 16 | // replaceAll not supported by node 14 so using .split.join 17 | .split('\\').join('/') 18 | })) 19 | }; 20 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/monorepo-to-output/files/tsconfig.json.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import deepmerge = require("deepmerge"); 3 | import { TS_CONFIG_JSON_ROOT_DIR, TS_CONFIG_JSON_FILENAME } from "../../../../common/constants"; 4 | import { MonorepoPackage } from "../../../../common/types/monorepo-package"; 5 | 6 | export function monorepoPakcageToTSConfigJsonOutput(monorepoPackage: MonorepoPackage): Object { 7 | return deepmerge( 8 | (monorepoPackage.config.files.json)[TS_CONFIG_JSON_FILENAME], 9 | { 10 | include: [ 11 | `${TS_CONFIG_JSON_ROOT_DIR}/**/*` 12 | ], 13 | references: Object.values(monorepoPackage.relationships.dependsOn) 14 | .map(dependencyMonorepoPackage => { 15 | // Calculate relative path to dependency from relative path of current. 16 | return { 17 | path: path.relative( 18 | path.resolve(monorepoPackage.relativePath), 19 | path.resolve(dependencyMonorepoPackage.relativePath) 20 | ) 21 | // If we don't use posix sep ('/' instead of windows '\'), then yarn + tsc can run into trouble resolving dependencies 22 | // replaceAll not supported by node 14 so using .split.join 23 | .split('\\').join('/') 24 | }; 25 | }) 26 | } 27 | ); 28 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/converters/monorepo-to-output/write-monorepo-package-files.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { MonorepoPackage } from "../../../common/types/monorepo-package"; 3 | import { writeJsonAndReportChanges } from "../../writers/json"; 4 | import { monorepoPackageToPackageJsonOutput } from './files/package.json'; 5 | import { monorepoPakcageToTSConfigJsonOutput } from './files/tsconfig.json'; 6 | import { assertDirectoryExistsOrCreate } from '../../../file-system/presence-assertions'; 7 | import { ConfigError } from '../../../common/errors'; 8 | import { TS_CONFIG_JSON_OUT_DIR, TS_CONFIG_JSON_ROOT_DIR, PACKAGE_JSON_FILENAME, TS_CONFIG_JSON_FILENAME, SUCCESS, Success } from '../../../common/constants'; 9 | import { MonorepoPackageRegistry } from '../../../package-dependency-logic/monorepo-package-registry'; 10 | import { CachedLatestVersionFetcher } from '../../cached-latest-version-fetcher'; 11 | import * as taskEither from 'fp-ts/lib/TaskEither'; 12 | import { pipe } from 'fp-ts/lib/pipeable'; 13 | import * as array from 'fp-ts/lib/Array'; 14 | import { taskEithercoalesceConfigErrors } from '../../error-coalesce'; 15 | import { writeIgnoreAndReportChanges } from '../../writers/ignore'; 16 | 17 | // TODO: come up with a more generic way to support file writers. Perhaps a plugin system? Eventually we want JSON, ignore, TOML, YAML, txt, pipfile, etc. 18 | export function writeMonorepoPackageFiles(monorepoPackage: MonorepoPackage, monorepoPackageRegistry: MonorepoPackageRegistry, 19 | latestVersionGetter: CachedLatestVersionFetcher): taskEither.TaskEither { 20 | return pipe( 21 | [ 22 | assertDirectoryExistsOrCreate(path.join(monorepoPackage.relativePath, TS_CONFIG_JSON_OUT_DIR)), 23 | assertDirectoryExistsOrCreate(path.join(monorepoPackage.relativePath, TS_CONFIG_JSON_ROOT_DIR)) 24 | ], 25 | taskEithercoalesceConfigErrors, 26 | // 1. JSON files 27 | taskEither.chain(() => pipe( 28 | Object.entries(monorepoPackage.config.files.json), 29 | array.map(([jsonFilename, jsonObject]) => { 30 | // Write templated config files 31 | const pathToFile = path.join(monorepoPackage.relativePath, jsonFilename); 32 | const outputObjectTaskEither = 33 | jsonFilename === PACKAGE_JSON_FILENAME ? monorepoPackageToPackageJsonOutput(monorepoPackage, monorepoPackageRegistry, latestVersionGetter) : 34 | jsonFilename === TS_CONFIG_JSON_FILENAME ? taskEither.right(monorepoPakcageToTSConfigJsonOutput(monorepoPackage)) : 35 | taskEither.right(jsonObject); 36 | return pipe( 37 | outputObjectTaskEither, 38 | taskEither.chain(outputObject => writeJsonAndReportChanges(pathToFile, outputObject)), 39 | taskEither.chain(() => taskEither.right(SUCCESS)) 40 | ); 41 | }), 42 | taskEithercoalesceConfigErrors 43 | )), 44 | // 2. ignore files (ie. .gitignore, .npmignore) 45 | taskEither.chain(() => pipe( 46 | Object.entries(monorepoPackage.config.files.ignore), 47 | array.map(([ignoreFileName, ignoreFileLines]) => { 48 | const pathToFile = path.join(monorepoPackage.relativePath, ignoreFileName); 49 | return pipe( 50 | writeIgnoreAndReportChanges(pathToFile, ignoreFileLines), 51 | taskEither.chain(() => taskEither.right(SUCCESS)) 52 | ); 53 | }), 54 | taskEithercoalesceConfigErrors 55 | )) 56 | // 3. TODO: support other file types. 57 | ); 58 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/deep-object-compare.ts: -------------------------------------------------------------------------------- 1 | import ansicolor = require("ansicolor"); 2 | 3 | type JSONPrimitive = string | number | boolean | null | Object | any[]; 4 | export function deepComparison(oldObj: any, newObj: any, keyChain: string): string[] { 5 | const oldObjKeys = Object.keys(oldObj); 6 | const newObjKeys = Object.keys(newObj); 7 | const fields = new Set([...oldObjKeys, ...newObjKeys]); 8 | const differences: string[] = []; 9 | function representation(colorizer: (input: string) => string, value?: JSONPrimitive) { 10 | if(value === undefined) return ""; 11 | if(typeof value === 'object') { 12 | if (Array.isArray(value)) { 13 | return "array of length " + value.length; 14 | } else { 15 | return " {...}"; 16 | } 17 | } else if (typeof value === 'string') { 18 | return ` "${colorizer(value)}"`; 19 | } else { 20 | return " " + colorizer(String(value)); 21 | } 22 | } 23 | function explainDifference(field: string, newValue?: JSONPrimitive, oldValue?: JSONPrimitive) { 24 | const action = 25 | newValue === undefined ? "removing" : 26 | oldValue === undefined ? " adding" : 27 | "changing"; 28 | differences.push(` ${action} ${keyChain}[${ansicolor.white(field)}]:${representation(ansicolor.lightMagenta, oldValue)}${representation(ansicolor.lightGreen, newValue)}`); 29 | } 30 | fields.forEach(field => { 31 | const oldField = oldObj[field]; 32 | const oldIsObject = typeof oldField === 'object'; 33 | 34 | const newField = newObj[field]; 35 | const newIsObject = typeof newField === 'object'; 36 | 37 | if (!oldIsObject && !newIsObject) { 38 | if (oldField !== newField) explainDifference(field, newField, oldField); 39 | } else if (oldIsObject !== newIsObject) { 40 | explainDifference(field, newField, oldField); 41 | } 42 | if (oldIsObject || newIsObject) { 43 | deepComparison(oldIsObject ? oldField : {}, newIsObject? newField : {}, keyChain + "[" + ansicolor.white(field) + "]") 44 | .forEach(difference => { 45 | differences.push(difference); 46 | }); 47 | } 48 | }); 49 | return differences; 50 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/error-coalesce.ts: -------------------------------------------------------------------------------- 1 | import { Either, fold as eFold, left as eLeft, right as eRight } from 'fp-ts/lib/Either'; 2 | import { TaskEither, fold as teFold, left as teLeft, right as teRight } from 'fp-ts/lib/TaskEither'; 3 | import { pipe } from 'fp-ts/lib/pipeable'; 4 | import { reduce } from 'fp-ts/lib/Array'; 5 | 6 | import { ConfigError } from '../common/errors'; 7 | import { Success, SUCCESS } from '../common/constants'; 8 | 9 | // TODO: switch to validations for error coalescing. 10 | export const taskEithercoalesceConfigErrors = (results: TaskEither[]): TaskEither => { 11 | return pipe( 12 | results, 13 | reduce(teRight(SUCCESS), (existingTaskEither, currentTaskEither) => { 14 | return pipe( 15 | currentTaskEither, 16 | teFold( 17 | currentErrors => pipe( 18 | existingTaskEither, 19 | teFold( 20 | existingErrors => teLeft([...currentErrors, ...existingErrors]), 21 | () => teLeft(currentErrors) 22 | ) 23 | ), 24 | () => existingTaskEither 25 | ) 26 | ) 27 | }) 28 | ); 29 | } 30 | 31 | export const taskEitherCoalesceConfigErrorsAndObject = (results: TaskEither[]): TaskEither => { 32 | return pipe( 33 | results, 34 | reduce(teRight([] as T[]), (accumulatorTaskEither, currentTaskEither) => pipe( 35 | currentTaskEither, 36 | teFold( 37 | currentErrors => pipe( 38 | accumulatorTaskEither, 39 | teFold( 40 | accumulationErrors => teLeft([...currentErrors, ...accumulationErrors]), 41 | () => teLeft(currentErrors) 42 | ) 43 | ), 44 | currentValues => pipe( 45 | accumulatorTaskEither, 46 | teFold( 47 | teLeft, 48 | accumulationValues => teRight([...accumulationValues, currentValues]) 49 | ) 50 | ) 51 | ) 52 | )) 53 | ); 54 | } 55 | 56 | export const eitherCoalesceConfigErrors = (results: Either[]): Either => { 57 | return pipe( 58 | results, 59 | reduce(eRight(SUCCESS), (existingEither, currentEither) => { 60 | return pipe( 61 | currentEither, 62 | eFold( 63 | currentErrors => pipe( 64 | existingEither, 65 | eFold( 66 | existingErrors => eLeft([...currentErrors, ...existingErrors]), 67 | () => eLeft(currentErrors) 68 | ) 69 | ), 70 | () => existingEither 71 | ) 72 | ); 73 | }) 74 | ); 75 | } 76 | 77 | // TODO: only use this form. 78 | export const eitherCoalesceConfigErrorsAndObject = (results: Either[]): Either => { 79 | return pipe( 80 | results, 81 | reduce(eRight([] as T[]), (accumulatorEither, currentEither) => pipe( 82 | currentEither, 83 | eFold( 84 | currentErrors => pipe( 85 | accumulatorEither, 86 | eFold( 87 | accumulationErrors => eLeft([...currentErrors, ...accumulationErrors]), 88 | () => eLeft(currentErrors) 89 | ) 90 | ), 91 | currentValues => pipe( 92 | accumulatorEither, 93 | eFold( 94 | eLeft, 95 | accumulationValues => eRight([...accumulationValues, currentValues]) 96 | ) 97 | ) 98 | ) 99 | )) 100 | ); 101 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/input-validation/validate-monorepo-config.ts: -------------------------------------------------------------------------------- 1 | import { MonorepoPackageRegistry } from "../../package-dependency-logic/monorepo-package-registry"; 2 | import { ConfigError, ErrorType } from "../../common/errors"; 3 | import { traversePackageTree, generateInitialContext, ConfigTreeTraversalContext } from "../traverse-package-tree"; 4 | import { validateAndMergeTemplates } from "./validate-templates"; 5 | import { colorize } from "../../colorize-special-text"; 6 | import { mergePackageConfig } from "../converters/input-to-merged/package-config"; 7 | import { SUCCESS, Success } from "../../common/constants"; 8 | import validateNpmPackageName = require('validate-npm-package-name'); 9 | import { validatePackageConfig } from "./validate-package-config"; 10 | import * as either from 'fp-ts/lib/Either'; 11 | import * as taskEither from 'fp-ts/lib/TaskEither'; 12 | import * as option from 'fp-ts/lib/Option'; 13 | import * as t from 'io-ts'; 14 | import { pipe } from 'fp-ts/function' 15 | import * as array from 'fp-ts/lib/Array'; 16 | import { taskEithercoalesceConfigErrors } from "../error-coalesce"; 17 | import { MergedPackageConfig } from "../../common/types/merged-config"; 18 | import { TSMonorepoJson, PackageConfig, JunctionConfig } from "../../common/types/io-ts/config-types"; 19 | import { constructPresentableConfigObjectPath } from "../../common/console-formatters/config-path"; 20 | 21 | const validatePackage = 22 | (monorepoConfig: t.TypeOf, seenResolvedPackageNamesToConfigObjectPath: Map, templates: Map, packageRegistry: MonorepoPackageRegistry) => 23 | (config: t.TypeOf, context: ConfigTreeTraversalContext, resolvedPackageName: string): taskEither.TaskEither => { 24 | const presentableConfigObjectPath = constructPresentableConfigObjectPath(context.configObjectPath); 25 | 26 | // First, the resulting package name must be valid 27 | const packageNameValidationResult = validateNpmPackageName(resolvedPackageName); 28 | if (!packageNameValidationResult.validForNewPackages) { 29 | const numberedErrors = packageNameValidationResult.errors ? 30 | packageNameValidationResult.errors.map((errorMessage, index) => `${index+1}. ${errorMessage}`).join("\n") 31 | : 32 | ""; 33 | return taskEither.left([{ 34 | type: ErrorType.InvalidPackageName, 35 | message: `Invalid package name ${colorize.package(resolvedPackageName)} which was resolved to by ${presentableConfigObjectPath}\n${numberedErrors}` 36 | }]); 37 | } 38 | 39 | // Second, there may not be any existing duplicate package names seen. 40 | const conflictingConfigObjectPath = seenResolvedPackageNamesToConfigObjectPath.get(resolvedPackageName); 41 | if (conflictingConfigObjectPath !== undefined) { 42 | return taskEither.left([{ 43 | type: ErrorType.DuplicateResolvedPackageName, 44 | message: `The package name ${colorize.package(resolvedPackageName)} is resolved to by both ${ 45 | presentableConfigObjectPath} and ${constructPresentableConfigObjectPath(conflictingConfigObjectPath)}` 46 | }]); 47 | } else { 48 | seenResolvedPackageNamesToConfigObjectPath.set(resolvedPackageName, context.configObjectPath); 49 | } 50 | 51 | return pipe( 52 | // Third, all of the templates that the package extends must be valid. 53 | config.extends, 54 | array.filter(extendsTemplateName => !templates.has(extendsTemplateName)), 55 | array.map(extendsTemplateName => !templates.has(extendsTemplateName) ? 56 | taskEither.left([{ 57 | type: ErrorType.NonExistentTemplate, 58 | message: `The package name ${colorize.package(resolvedPackageName)} extends non-existent template "${colorize.template(extendsTemplateName)}".` 59 | }]) : 60 | taskEither.right(SUCCESS) 61 | ), 62 | taskEithercoalesceConfigErrors, 63 | // Fourth, the package config itself should have valid contents (tsconfig.json and package.json have some values which this tool must set itself, and so the use may not set those values) 64 | taskEither.chain(() => taskEither.fromEither(validatePackageConfig(config, `In package ${colorize.package(resolvedPackageName)} at ${presentableConfigObjectPath}`))), 65 | // Merge configs with templates. 66 | taskEither.map(() => mergePackageConfig(templates, config, resolvedPackageName, monorepoConfig.version)), 67 | // Register dependencies. 68 | taskEither.chain(mergedConfig => taskEither.fromEither(packageRegistry.registerPackage(mergedConfig, context.relativePath))) 69 | ); 70 | } 71 | 72 | const validateJunction = 73 | (_config: t.TypeOf, context: ConfigTreeTraversalContext, childContexts: Record): taskEither.TaskEither => { 74 | const presentableConfigObjectPath = constructPresentableConfigObjectPath(context.configObjectPath); 75 | // Validate the children of the junction (subfolders) names. There validations are related to how the children package name segments map to sub-folder names. 76 | const pathSegmentToNameSegment: Map = new Map(); 77 | return pipe( 78 | Object.entries(childContexts), 79 | array.map(([childNameSegment, childContext]) => { 80 | // Here we ensure that no two child config name segments correspond to the same sub-folder name. 81 | // This can happen since we remove the non alphanumeric characters from the beginning of the package name segment before creating the folder. 82 | const conflictingNameSegment = pathSegmentToNameSegment.get(childContext.pathSegment); 83 | if (conflictingNameSegment !== undefined) { 84 | return taskEither.left([{ 85 | type: ErrorType.DuplicateSubfolder, 86 | message: `Issue with package junction at ${presentableConfigObjectPath}. The two children with names ${ 87 | colorize.package(conflictingNameSegment)} and ${colorize.package(childNameSegment) 88 | } correspond to a sub-folder named ${colorize.directory(childContext.pathSegment) 89 | }. One of the names must be changed or removed.` 90 | }]); 91 | } else { 92 | pathSegmentToNameSegment.set(childContext.pathSegment, childNameSegment); 93 | } 94 | // Here we ensure the resulting subfolder name isn't an empty string. You can't have a folder with an empty name. We do the check here instead of at the start of the validation functions 95 | // because it's a validation universal to both junction and package config. 96 | if (childContext.pathSegment.length === 0) { 97 | return taskEither.left([{ 98 | type: ErrorType.SubfolderIsEmptyString, 99 | message: `A key of package junction at ${presentableConfigObjectPath} resolves to an empty string.\nThe key "${colorize.package(childNameSegment)}" resolves to an empty string sub-folder which is illegal.` 100 | }]); 101 | } 102 | return taskEither.right(SUCCESS); 103 | }), 104 | taskEithercoalesceConfigErrors 105 | ); 106 | } 107 | 108 | // See https://blog.npmjs.org/post/168978377570/new-package-moniker-rules 109 | function ensureNoPackagesEquivalenGivenMonikerRule(packageRegistry: MonorepoPackageRegistry): either.Either { 110 | return pipe( 111 | Array.from(packageRegistry.getRegisteredPackages().values()), 112 | array.map(monorepoPackage => monorepoPackage.name), 113 | array.reduce(either.right>(new Map()), (result, currentMonorepoPackageName) => pipe( 114 | result, 115 | either.chain(seenPackageNamesWithoutPunctuation => { 116 | const currentMonorepoPackageNameSplitByScopeDivider = currentMonorepoPackageName.split('/'); 117 | const currentMonorepoPackageNameWithoutScope = currentMonorepoPackageNameSplitByScopeDivider[currentMonorepoPackageNameSplitByScopeDivider.length - 1]; 118 | const currentMonorepoPackageNameWithoutPunctuation = currentMonorepoPackageNameWithoutScope.replace(/[^a-zA-Z\d]/g, ''); 119 | const otherPackageNameWithEquivalentPunctionRemovedName = seenPackageNamesWithoutPunctuation.get(currentMonorepoPackageNameWithoutPunctuation); 120 | if (otherPackageNameWithEquivalentPunctionRemovedName !== undefined) { 121 | return either.left([{ 122 | type: ErrorType.DuplicateResolvedPackageName, 123 | message: `Two packages:\n1. ${colorize.package(otherPackageNameWithEquivalentPunctionRemovedName)}\n2. ${colorize.package(currentMonorepoPackageName) 124 | }\nhave the same package name ${colorize.package(currentMonorepoPackageNameWithoutPunctuation)} after removing punction & scope.${ 125 | "\n"}This is denied by npm. See https://blog.npmjs.org/post/168978377570/new-package-moniker-rules` 126 | } as ConfigError]); 127 | } else { 128 | seenPackageNamesWithoutPunctuation.set(currentMonorepoPackageNameWithoutPunctuation, currentMonorepoPackageName); 129 | return either.right(seenPackageNamesWithoutPunctuation); 130 | } 131 | }) 132 | )), 133 | either.map(() => SUCCESS) 134 | ) 135 | } 136 | 137 | export function validateMonorepoConfig(monorepoConfig: t.TypeOf, packageRegistry: MonorepoPackageRegistry): taskEither.TaskEither { 138 | return pipe( 139 | option.fromNullable(monorepoConfig.templates), 140 | either.fromOption(() => new Map()), 141 | either.fold(either.right, validateAndMergeTemplates), 142 | taskEither.fromEither, 143 | taskEither.chain(mergedTemplates => pipe( 144 | option.fromNullable(monorepoConfig.packages), 145 | either.fromOption(() => SUCCESS), 146 | either.fold( 147 | taskEither.right, 148 | packages => pipe( 149 | Object.entries(packages), 150 | array.map(([scope, packagesUnderScope]) => traversePackageTree( 151 | packagesUnderScope, 152 | generateInitialContext(scope), 153 | validatePackage(monorepoConfig, new Map(), mergedTemplates, packageRegistry), 154 | validateJunction 155 | )), 156 | taskEithercoalesceConfigErrors 157 | ) 158 | ) 159 | )), 160 | taskEither.chain(() => pipe( 161 | ensureNoPackagesEquivalenGivenMonikerRule(packageRegistry), 162 | either.chain(() => packageRegistry.ensureNoCircularDependencies()), 163 | taskEither.fromEither 164 | )) 165 | ); 166 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/input-validation/validate-package-config.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { ConfigError } from "../../common/errors"; 3 | import { validatePackageJson } from "./validate-package.json"; 4 | import { validateTSConfigJson } from "./validate-tsconfig.json"; 5 | import * as either from "fp-ts/lib/Either"; 6 | import { Success, SUCCESS } from "../../common/constants"; 7 | import { PackageConfig } from '../../common/types/io-ts/config-types'; 8 | import { pipe } from 'fp-ts/lib/pipeable'; 9 | import { eitherCoalesceConfigErrors } from '../error-coalesce'; 10 | 11 | export function validatePackageConfig(packageConfig: t.TypeOf, configLocation: string): either.Either { 12 | if (packageConfig.files && packageConfig.files.json) { 13 | const packageJson = packageConfig.files.json["package.json"]; 14 | const tsConfigJson = packageConfig.files.json["tsconfig.json"]; 15 | return pipe( 16 | [ 17 | (packageJson ? validatePackageJson(packageJson, configLocation) : either.right(SUCCESS)), 18 | (tsConfigJson ? validateTSConfigJson(tsConfigJson, configLocation) : either.right(SUCCESS)) 19 | ], 20 | eitherCoalesceConfigErrors 21 | ); 22 | } 23 | return either.right(SUCCESS); 24 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/input-validation/validate-package.json.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import * as either from 'fp-ts/lib/Either'; 3 | import * as array from 'fp-ts/lib/Array'; 4 | import { ConfigError, ErrorType } from "../../common/errors"; 5 | import { PACKAGES_DIRECTORY_NAME, Success, SUCCESS } from "../../common/constants"; 6 | import { colorize } from "../../colorize-special-text"; 7 | import { PartialPackageJson, NodeDependency } from '../../common/types/io-ts/config-types'; 8 | import { pipe } from 'fp-ts/lib/pipeable'; 9 | import { eitherCoalesceConfigErrors } from '../error-coalesce'; 10 | 11 | // TODO: when validating, ensure that client has not explicitly set this (similar to tsconfig.json). Also consider making such logic & validations more generic. 12 | export const MANDATORY_PACKAGE_JSON_VALUES = {}; 13 | 14 | function validateDependencies(dependencies: t.TypeOf[] | undefined, dependencyField: string, configLocation: string): either.Either { 15 | if (dependencies) { 16 | const seenDeps = new Set(); 17 | const depsWithError = new Set(); 18 | return pipe( 19 | dependencies, 20 | array.map(dependency => { 21 | const dependencyName = Array.isArray(dependency) ? dependency[0] : dependency; 22 | if (seenDeps.has(dependencyName)) { 23 | if (!depsWithError.has(dependencyName)) { 24 | depsWithError.add(dependencyName); 25 | return either.left([{ 26 | type: ErrorType.DuplicateDependencyInPackageJsonConfig, 27 | message: `${configLocation} the package.json ${dependencyField} has multiple entries for package ${colorize.package(dependencyName)}` 28 | }]) 29 | 30 | } 31 | } else { 32 | seenDeps.add(dependencyName); 33 | } 34 | return either.right(SUCCESS); 35 | }), 36 | eitherCoalesceConfigErrors 37 | ) 38 | } 39 | return either.right(SUCCESS); 40 | } 41 | 42 | export function validatePackageJson(packageJson: t.TypeOf, configLocation: string): either.Either { 43 | return pipe( 44 | [ 45 | (() => { 46 | const packageJsonWithAnyPossibleValue = packageJson as any; 47 | if ('name' in packageJsonWithAnyPossibleValue) { 48 | return either.left([{ 49 | type: ErrorType.ExplicitNameInPackageJsonConfig, 50 | message: `${configLocation} the package.json illegally specifies an explicit name '${packageJsonWithAnyPossibleValue.name 51 | }'. Package names will be written to output package.json files by contatenating nested keys under the ${PACKAGES_DIRECTORY_NAME} object.` 52 | }]); 53 | } 54 | return either.right(SUCCESS); 55 | })(), 56 | validateDependencies(packageJson.dependencies, 'dependencies', configLocation), 57 | validateDependencies(packageJson.devDependencies, 'devDependencies', configLocation), 58 | validateDependencies(packageJson.optionalDependencies, 'optionalDependencies', configLocation), 59 | validateDependencies(packageJson.peerDependencies, 'peerDependencies', configLocation) 60 | ], 61 | eitherCoalesceConfigErrors 62 | ); 63 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/input-validation/validate-scope.ts: -------------------------------------------------------------------------------- 1 | import validateNpmPackageName = require('validate-npm-package-name'); 2 | import { GLOBAL_SCOPE_NAME, Success, SUCCESS, CONFIG_FILE_NAME, PACKAGES_DIRECTORY_NAME } from '../../common/constants'; 3 | import { colorize } from '../../colorize-special-text'; 4 | import { ConfigError, ErrorType } from '../../common/errors'; 5 | import { Either, right, left } from 'fp-ts/lib/Either'; 6 | 7 | export function validateScope(scope: string): Either { 8 | // If the package belongs under no scope then they should be placed under the global scope name. 9 | if (scope !== GLOBAL_SCOPE_NAME) { 10 | const invalidScopePrefix = `Keys of ${colorize.file(CONFIG_FILE_NAME)}[${PACKAGES_DIRECTORY_NAME}] must be npm scopes.\nScope "${colorize.scope(scope)}" is not a valid scope.`; 11 | const invalidScopeSuffix = `\nTo configure packages without a scope, use the key "${colorize.scope(GLOBAL_SCOPE_NAME)}".` 12 | const result = validateNpmPackageName(`${scope}/test`); 13 | if (!result.validForNewPackages) { 14 | return left([{ 15 | type: ErrorType.InvalidScope, 16 | message: `${invalidScopePrefix}${invalidScopeSuffix}` 17 | }]); 18 | } 19 | } 20 | return right(SUCCESS); 21 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/input-validation/validate-templates.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { PackageConfig } from '../../common/types/io-ts/config-types'; 3 | import { MergedPackageConfig } from "../../common/types/merged-config"; 4 | import { ConfigError, ErrorType } from "../../common/errors"; 5 | import { colorize } from "../../colorize-special-text"; 6 | import { mergePackageConfig } from "../converters/input-to-merged/package-config"; 7 | import { validatePackageConfig } from "./validate-package-config"; 8 | import { Either, left, right, chain } from 'fp-ts/lib/Either'; 9 | import { pipe } from "fp-ts/lib/pipeable"; 10 | import { map as arrayMap } from 'fp-ts/lib/Array'; 11 | import { eitherCoalesceConfigErrors } from "../error-coalesce"; 12 | import { SUCCESS, Success } from "../../common/constants"; 13 | 14 | function recursivelyEnsureTemplateHasNoCircularDependencies(templatePath: string[], templates: {[name: string]: t.TypeOf}): Either { 15 | if (templatePath.length === 0) return right(SUCCESS); 16 | const currentTemplateName = templatePath[templatePath.length - 1]; 17 | return pipe( 18 | templates[currentTemplateName].extends, 19 | arrayMap((currentTemplateDependency) => templatePath.includes(currentTemplateDependency) ? 20 | left([{ 21 | type: ErrorType.CircularTemplateDependency, 22 | message: `The template ${colorize.template(currentTemplateName)} circularly depends on the template ${colorize.template(currentTemplateDependency)}` 23 | }]) : 24 | recursivelyEnsureTemplateHasNoCircularDependencies([...templatePath, currentTemplateDependency], templates) 25 | ), 26 | eitherCoalesceConfigErrors 27 | ); 28 | } 29 | 30 | export function validateAndMergeTemplates(templates: {[name: string]: t.TypeOf}): Either> { 31 | const templateEntries = Object.entries(templates); 32 | return pipe( 33 | // 1. Validate template contents to prevent illegal settings. 34 | pipe( 35 | templateEntries, 36 | arrayMap(([templateName, templateConfig]) => validatePackageConfig( 37 | templateConfig, 38 | `In the template ${colorize.template(templateName)}` 39 | )), 40 | eitherCoalesceConfigErrors 41 | ), 42 | // 2. Ensure all templates which are referenced in template's extends actually exist 43 | chain(() => pipe( 44 | templateEntries, 45 | arrayMap(([templateName, templateConfig]) => pipe( 46 | templateConfig.extends, 47 | arrayMap((templateExtends) => templates[templateExtends] ? 48 | right(SUCCESS) : left([{ 49 | type: ErrorType.NonExistentTemplate, 50 | message: `The template ${colorize.template(templateName)} extends non-existent template "${colorize.template(templateExtends)}".` 51 | } as ConfigError]) 52 | ), 53 | eitherCoalesceConfigErrors, 54 | )), 55 | eitherCoalesceConfigErrors 56 | )), 57 | // 3. Ensure there are no circular dependencies in the templates. 58 | chain(() => pipe( 59 | templateEntries, 60 | arrayMap(([templateName]) => recursivelyEnsureTemplateHasNoCircularDependencies([templateName], templates)), 61 | eitherCoalesceConfigErrors 62 | )), 63 | // 4. Now merge all templates 64 | chain(() => { 65 | const mergedTemplatesMap: Map = new Map(); 66 | for (var currentTemplateEntry = templateEntries.shift(); currentTemplateEntry !== undefined; currentTemplateEntry = templateEntries.shift()) { 67 | const [templateName, templateConfig] = currentTemplateEntry; 68 | // If all dependencies are in the merged map already. 69 | if (templateConfig.extends.filter(mergedTemplatesMap.has.bind(mergedTemplatesMap)).length === templateConfig.extends.length) { 70 | mergedTemplatesMap.set(templateName, mergePackageConfig(mergedTemplatesMap, templateConfig)); 71 | } else { 72 | templateEntries.push(currentTemplateEntry); 73 | } 74 | } 75 | return right(mergedTemplatesMap); 76 | }) 77 | ); 78 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/input-validation/validate-tsconfig.json.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import * as either from 'fp-ts/lib/Either'; 3 | import * as array from 'fp-ts/lib/Array'; 4 | import { PartialTSConfigJson } from '../../common/types/io-ts/config-types'; 5 | import { ConfigError, ErrorType } from "../../common/errors"; 6 | import { TOOL_SHORT_NAME, TS_CONFIG_JSON_ROOT_DIR, TS_CONFIG_JSON_OUT_DIR, Success, SUCCESS } from "../../common/constants"; 7 | import { pipe } from 'fp-ts/lib/pipeable'; 8 | import { eitherCoalesceConfigErrors } from '../error-coalesce'; 9 | import { colorize } from '../../colorize-special-text'; 10 | 11 | export const MandatoryTSConfigJsonValues = { 12 | compilerOptions: { 13 | rootDir: TS_CONFIG_JSON_ROOT_DIR, 14 | outDir: TS_CONFIG_JSON_OUT_DIR, 15 | // See https://github.com/RyanCavanaugh/learn-a#tsconfigsettingsjson 16 | composite: true, 17 | declaration: true, 18 | declarationMap: true, 19 | sourceMap: true 20 | }, 21 | references: [] 22 | }; 23 | 24 | function ensureValueNotSetExplicitly(field: string, compilerOptions: boolean, errorType: ErrorType, tsConfig: t.TypeOf, configLocation: string): either.Either { 25 | function generateError(fullFieldName: string): ConfigError { 26 | return { 27 | type: errorType, 28 | message: `${configLocation} the ${colorize.file('tsconfig.json')} config illegally sets field '${colorize.type(fullFieldName)}' explicitly. ${colorize.package(TOOL_SHORT_NAME)} will set this for you.` 29 | } 30 | } 31 | if (compilerOptions) { 32 | if (field in tsConfig.compilerOptions) { 33 | return either.left([generateError(`compilerOptions.${field}`)]); 34 | } 35 | } else { 36 | if (field in tsConfig) { 37 | return either.left([generateError(field)]); 38 | } 39 | } 40 | return either.right(SUCCESS); 41 | } 42 | 43 | // TODO: validate based on the non-overrideable settings 44 | export function validateTSConfigJson(tsConfig: t.TypeOf, configLocation: string): either.Either { 45 | return pipe( 46 | [ 47 | (() => { 48 | if (tsConfig.compilerOptions) { 49 | return pipe( 50 | Object.keys(MandatoryTSConfigJsonValues.compilerOptions), 51 | array.map(mandatoryValueKey => 52 | ensureValueNotSetExplicitly(mandatoryValueKey, true, ErrorType.ExplicitlySetNonOverridableValueInTSConfigJson, tsConfig, configLocation)), 53 | eitherCoalesceConfigErrors 54 | ); 55 | } 56 | return either.right(SUCCESS); 57 | })(), 58 | ensureValueNotSetExplicitly('references', false, ErrorType.ExplicitlySetNonOverridableValueInTSConfigJson, tsConfig, configLocation) 59 | ], 60 | eitherCoalesceConfigErrors 61 | ); 62 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/sync-monorepo.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as taskEither from 'fp-ts/lib/TaskEither'; 3 | import * as either from 'fp-ts/lib/Either'; 4 | import * as option from 'fp-ts/lib/Option'; 5 | import { pipe } from 'fp-ts/lib/pipeable'; 6 | import * as array from 'fp-ts/lib/Array'; 7 | import { assertFileSystemObjectType, assertDirectoryExistsOrCreate } from '../file-system/presence-assertions'; 8 | import { CONFIG_FILE_RELATIVE_PATH, CONFIG_FILE_ABSOLUTE_PATH, PACKAGES_DIRECTORY_RELATIVE_PATH, TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_RELATIVE_PATH, TS_MONOREPO_FOLDER_RELATIVE_PATH, SUCCESS, Success, MONOREPO_PACKAGE_JSON_RELATIVE_PATH } from '../common/constants'; 9 | import { parseJson } from '../file-system/parse-json'; 10 | import { ConfigError, ErrorType } from '../common/errors'; 11 | import { Terminateable } from '../common/types/traits'; 12 | import { traversePackageTree, generateInitialContext } from './traverse-package-tree'; 13 | import { MonorepoPackageRegistry } from '../package-dependency-logic/monorepo-package-registry'; 14 | import { validateMonorepoConfig } from './input-validation/validate-monorepo-config'; 15 | import { FileSystemObjectType } from '../file-system/object'; 16 | import { writeMonorepoPackageFiles } from './converters/monorepo-to-output/write-monorepo-package-files'; 17 | import { writeJsonAndReportChanges } from './writers/json'; 18 | import { monorepoPackageRegistryToTSProjectLeavesJsonOutput } from './converters/monorepo-to-output/files/ts-project-leaves.json'; 19 | import { CachedLatestVersionFetcher } from './cached-latest-version-fetcher'; 20 | import { taskEithercoalesceConfigErrors } from './error-coalesce'; 21 | import { validateTSMonoRepoJsonShape } from '../common/types/io-ts/config-types'; 22 | import { validateNoUnexpectedFolders } from './validate-no-unexpected-folders'; 23 | import { installViaBerry } from '../package-dependency-logic/berry-install/install-with-berry'; 24 | import { startTypeScript } from '../process/typescript-runner'; 25 | import { monorepoPackageRegistryToMonorepoRootPackageJson } from './converters/monorepo-to-output/files/monorepo-package.json'; 26 | 27 | export function syncMonorepo(): taskEither.TaskEither { 28 | const packageRegistry = new MonorepoPackageRegistry(); 29 | const cachedLatestVersionFetcher = new CachedLatestVersionFetcher(); 30 | 31 | // TODO: pass comments down to generated jsons (for json which supports this) 32 | // TODO: get a source map from loaded json object path to location range in json file. This will allow for better error reporting and eventual VSCode plugin. 33 | 34 | return pipe( 35 | // 1. Read in monorepo config 36 | assertFileSystemObjectType(CONFIG_FILE_RELATIVE_PATH, [FileSystemObjectType.file]), 37 | taskEither.chain(() => async () => either.right((await fs.promises.readFile(CONFIG_FILE_ABSOLUTE_PATH)).toString())), 38 | taskEither.chain(json => taskEither.fromEither(parseJson(json))), // Switch to chainEither when 2.4 comes out 39 | taskEither.chain(validateTSMonoRepoJsonShape), 40 | taskEither.chain(monorepoConfig => pipe( 41 | // 2. Validate config file 42 | validateMonorepoConfig(monorepoConfig, packageRegistry), 43 | taskEither.chain(() => { 44 | if (packageRegistry.getLeafSet().size === 0) { 45 | return taskEither.left([{ 46 | type: ErrorType.NoLeafPackages, 47 | message: "No leaf packages detected. There must be at least one." 48 | }]) 49 | } else { 50 | return taskEither.right(SUCCESS); 51 | } 52 | }), 53 | // 3. Write directories 54 | taskEither.chain(() => pipe( 55 | option.fromNullable(monorepoConfig.packages), 56 | either.fromOption(() => SUCCESS), 57 | taskEither.fromEither, 58 | taskEither.fold( 59 | taskEither.right, 60 | packages => pipe( 61 | assertDirectoryExistsOrCreate(PACKAGES_DIRECTORY_RELATIVE_PATH), 62 | taskEither.chain(() => pipe( 63 | Object.entries(packages), 64 | array.map(([scopeName, packagesUnderScope]) => traversePackageTree( 65 | packagesUnderScope, 66 | generateInitialContext(scopeName), 67 | (_config, context) => assertDirectoryExistsOrCreate(context.relativePath), 68 | (_config, context) => assertDirectoryExistsOrCreate(context.relativePath) 69 | )), 70 | taskEithercoalesceConfigErrors 71 | )) 72 | ) 73 | ) 74 | )), 75 | // 4. Make sure no unexpected folders exist 76 | taskEither.chain(() => validateNoUnexpectedFolders(monorepoConfig)), // TODO: add watchers so that if unexpected objects are removed, then sync is rerun. 77 | // 5. Write files. 78 | taskEither.chain(() => pipe( 79 | Array.from(packageRegistry.getRegisteredPackages()), 80 | array.map(monorepoPackage => writeMonorepoPackageFiles(monorepoPackage, packageRegistry, cachedLatestVersionFetcher)), 81 | taskEithercoalesceConfigErrors 82 | )), 83 | taskEither.chain(() => pipe( 84 | assertDirectoryExistsOrCreate(TS_MONOREPO_FOLDER_RELATIVE_PATH), 85 | taskEither.chain(() => writeJsonAndReportChanges( 86 | TYPESCRIPT_LEAF_PACKAGES_CONFIG_FILE_RELATIVE_PATH, 87 | monorepoPackageRegistryToTSProjectLeavesJsonOutput(packageRegistry) 88 | )), 89 | taskEither.chain(() => monorepoPackageRegistryToMonorepoRootPackageJson(packageRegistry)), 90 | taskEither.chain(packageJsonObject => writeJsonAndReportChanges( 91 | MONOREPO_PACKAGE_JSON_RELATIVE_PATH, 92 | packageJsonObject 93 | )), 94 | )), 95 | // 6. Install dependencies. 96 | taskEither.chain(installViaBerry), 97 | // 7. Set up watchers 98 | taskEither.map(() => { 99 | return startTypeScript(monorepoConfig); 100 | }) 101 | )) 102 | ); 103 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/traverse-package-tree.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { ConfigError } from '../common/errors'; 3 | import { PACKAGE_NAME_CONFIG_PATH_REQUIRED_SUFFIX, GLOBAL_SCOPE_NAME, PACKAGES_DIRECTORY_RELATIVE_PATH, PACKAGES_DIRECTORY_NAME, Success } from '../common/constants'; 4 | import * as taskEither from 'fp-ts/lib/TaskEither'; 5 | import * as t from 'io-ts'; 6 | import * as array from 'fp-ts/lib/Array'; 7 | import { pipe } from 'fp-ts/lib/function'; 8 | import { taskEithercoalesceConfigErrors } from './error-coalesce'; 9 | import { validatePackageConfig, PackageConfig, JunctionConfig, validateJunctionConfig } from '../common/types/io-ts/config-types'; 10 | 11 | const lowercaseAlphanumeric = /[a-z0-9]/; 12 | function removeLeadingPunctuation(packageNameSegment: string): string { 13 | for (var currentIndex = 0; currentIndex < packageNameSegment.length; ++currentIndex) { 14 | if (packageNameSegment[currentIndex].match(lowercaseAlphanumeric)) { 15 | return packageNameSegment.slice(currentIndex); 16 | } 17 | } 18 | return ""; 19 | } 20 | 21 | export const nameSegmentToSubFolderName = removeLeadingPunctuation; 22 | 23 | export interface ConfigTreeTraversalContext { 24 | packageNamePrefix: string; 25 | relativePath: string; 26 | pathSegment: string; 27 | configObjectPath: string[]; 28 | packageNameSegment: string; 29 | } 30 | 31 | export function generateInitialContext(scope: string) { 32 | const scopePackageNameSegment = scope === GLOBAL_SCOPE_NAME ? "" : `${scope}/`; 33 | const initialContext: ConfigTreeTraversalContext = { 34 | packageNamePrefix: scopePackageNameSegment, 35 | relativePath: path.join(PACKAGES_DIRECTORY_RELATIVE_PATH, scope), 36 | configObjectPath: [PACKAGES_DIRECTORY_NAME, scope], 37 | pathSegment: scope, 38 | packageNameSegment: scopePackageNameSegment 39 | }; 40 | return initialContext; 41 | } 42 | 43 | function resolvePackageNameFromContext(context: ConfigTreeTraversalContext) { 44 | return context.packageNamePrefix.substring(0, context.packageNamePrefix.length - PACKAGE_NAME_CONFIG_PATH_REQUIRED_SUFFIX.length); 45 | } 46 | 47 | export function traversePackageTree( 48 | config: unknown, 49 | context: ConfigTreeTraversalContext, 50 | packageHandler: (packageConfig: t.TypeOf, context: ConfigTreeTraversalContext, completePackageName: string) => taskEither.TaskEither, 51 | junctionHandler: (junctionConfig: t.TypeOf, context: ConfigTreeTraversalContext, childContexts: Record) => taskEither.TaskEither 52 | ): taskEither.TaskEither { 53 | const isPackageConfig = context.packageNamePrefix.endsWith(PACKAGE_NAME_CONFIG_PATH_REQUIRED_SUFFIX); 54 | if (isPackageConfig) { 55 | return pipe( 56 | validatePackageConfig(context.configObjectPath)(config), 57 | taskEither.fromEither, 58 | taskEither.chain(packageConfig => packageHandler(packageConfig, context, resolvePackageNameFromContext(context))) 59 | ); 60 | } else { 61 | return pipe( 62 | validateJunctionConfig(context.configObjectPath)(config), 63 | taskEither.fromEither, 64 | taskEither.chain(junctionConfig => { 65 | const childContexts = pipe( 66 | Object.keys(junctionConfig), 67 | array.map(nameSegment => { 68 | const pathSegment = nameSegmentToSubFolderName(nameSegment); 69 | return [ 70 | nameSegment, 71 | { 72 | packageNamePrefix: `${context.packageNamePrefix}${nameSegment}`, 73 | relativePath: path.join(context.relativePath, pathSegment), 74 | configObjectPath: [...context.configObjectPath, nameSegment], 75 | pathSegment, 76 | packageNameSegment: nameSegment 77 | } 78 | ]; 79 | }), 80 | Object.fromEntries 81 | ); 82 | return pipe( 83 | junctionHandler(junctionConfig, context, childContexts), 84 | taskEither.chain(() => pipe( 85 | Object.entries(junctionConfig), 86 | array.map(([nameSegment, childConfig]) => traversePackageTree( 87 | childConfig, 88 | childContexts[nameSegment], 89 | packageHandler, junctionHandler 90 | )), 91 | taskEithercoalesceConfigErrors 92 | )) 93 | ); 94 | }) 95 | ); 96 | } 97 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/validate-no-unexpected-folders.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import * as taskEither from 'fp-ts/lib/TaskEither'; 3 | import * as either from 'fp-ts/lib/Either'; 4 | import * as array from 'fp-ts/lib/Array'; 5 | import { ConfigError, ErrorType } from '../common/errors'; 6 | import * as path from 'path'; 7 | import * as fs from 'fs'; 8 | import { Success, PACKAGES_DIRECTORY_RELATIVE_PATH, SUCCESS, CONFIG_FILE_NAME } from '../common/constants'; 9 | import { pipe } from 'fp-ts/lib/pipeable'; 10 | import { FileSystemObjectType, getFileSystemObjectDescriptor, FileSystemObjectDescriptor } from '../file-system/object'; 11 | import { assertFileSystemObjectType } from '../file-system/presence-assertions'; 12 | import { TSMonorepoJson } from '../common/types/io-ts/config-types'; 13 | import { colorize } from '../colorize-special-text'; 14 | import { traversePackageTree, generateInitialContext, nameSegmentToSubFolderName } from './traverse-package-tree'; 15 | import { taskEithercoalesceConfigErrors } from './error-coalesce'; 16 | 17 | function generateUnexpectedFolderError(relativePath: string, descriptor: FileSystemObjectDescriptor): ConfigError { 18 | const colorizedRelativePath = 19 | descriptor.type === FileSystemObjectType.file ? colorize.file(relativePath) : 20 | descriptor.type === FileSystemObjectType.directory ? colorize.directory(relativePath) : 21 | descriptor.type === FileSystemObjectType.symlink ? colorize.symlink(relativePath) : undefined; 22 | if (colorizedRelativePath === undefined) { 23 | throw new Error(`Unexpected type ${descriptor.type} for path ${descriptor.path}. Logical program error`); 24 | } 25 | // If it's not a directory, there is no valid encoding for this object. If it is a directory, then possibly author forgot to encode for package or junction. 26 | const isDirectory = descriptor.type === FileSystemObjectType.directory; 27 | const encodeForItMessage = isDirectory ? ` or encode for it in ${colorize.file(CONFIG_FILE_NAME)} ` : " "; 28 | return { 29 | type: ErrorType.UnexpectedFilesystemObject, 30 | message: `Found unexpected ${descriptor.type} at ${colorizedRelativePath}.${ 31 | "\n"}Please remove it${encodeForItMessage}before continuing.${ 32 | isDirectory ? "\nIf a package name segment was renamed, make sure to move over all source code before deleting the directory." : ""}` 33 | }; 34 | } 35 | 36 | export const validateNoUnexpectedFolders = (monorepoConfig: t.TypeOf): taskEither.TaskEither => { 37 | const packages = monorepoConfig.packages ?? {}; 38 | const validScopes = Object.keys(packages); 39 | return pipe( 40 | assertFileSystemObjectType(PACKAGES_DIRECTORY_RELATIVE_PATH, [FileSystemObjectType.nothing, FileSystemObjectType.directory]), 41 | taskEither.chain(descriptor => { 42 | if (descriptor.type === FileSystemObjectType.nothing) { 43 | return taskEither.right([]); // Empty array meaning no children of packages directory 44 | } 45 | // Get children of the packages directory. 46 | return async () => either.right(await fs.promises.readdir(path.resolve(PACKAGES_DIRECTORY_RELATIVE_PATH))); 47 | }), 48 | taskEither.chain(packagesDirectoryChildren => pipe( 49 | packagesDirectoryChildren, 50 | array.map(packagesDirectoryChild => ({ 51 | possibleScope: packagesDirectoryChild, 52 | descriptorPromise: getFileSystemObjectDescriptor(path.resolve(path.join(PACKAGES_DIRECTORY_RELATIVE_PATH, packagesDirectoryChild))) 53 | })), 54 | array.map(({possibleScope, descriptorPromise}) => pipe( 55 | !validScopes.includes(possibleScope) ? 56 | async () => either.left([generateUnexpectedFolderError(path.join(PACKAGES_DIRECTORY_RELATIVE_PATH, possibleScope), await descriptorPromise)]) : 57 | taskEither.right(SUCCESS), 58 | taskEither.chain(() => traversePackageTree( 59 | packages[possibleScope], 60 | generateInitialContext(possibleScope), 61 | () => taskEither.right(SUCCESS), 62 | (junctionConfig, context, _childContexts) => async () => { 63 | const validSubfolders = Object.keys(junctionConfig).map(nameSegmentToSubFolderName); 64 | return await pipe( 65 | await fs.promises.readdir(path.resolve(context.relativePath)), 66 | array.map(junctionSubFolder => async() => 67 | !validSubfolders.includes(junctionSubFolder) ? 68 | either.left([generateUnexpectedFolderError( 69 | path.join(context.relativePath, junctionSubFolder), 70 | await getFileSystemObjectDescriptor(path.resolve(path.join(context.relativePath, junctionSubFolder))) 71 | )]) : 72 | either.right(SUCCESS) 73 | ), 74 | taskEithercoalesceConfigErrors 75 | )(); 76 | } 77 | )) 78 | )), 79 | taskEithercoalesceConfigErrors 80 | )) 81 | ); 82 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/writers/ignore.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { EOL } from 'os'; 4 | 5 | import { log } from '../../logging/log'; 6 | 7 | import { deepComparison } from '../deep-object-compare'; 8 | import { ConfigError } from '../../common/errors'; 9 | import { assertFileSystemObjectType } from '../../file-system/presence-assertions'; 10 | import { FileSystemObjectType } from '../../file-system/object'; 11 | import { colorize } from '../../colorize-special-text'; 12 | import { TaskEither, chain } from 'fp-ts/lib/TaskEither'; 13 | import { right } from 'fp-ts/lib/Either'; 14 | import { pipe } from 'fp-ts/lib/function'; 15 | 16 | // Examples of uses: .npmignore, .gitignore 17 | // TODO: in the future use the later lines to cancel out earlier lines if they dictate contradictory rules. 18 | export function writeIgnoreAndReportChanges(relativePath: string, outputLines: string[]): TaskEither { 19 | const absolutePath = path.resolve(relativePath); 20 | const outputString = outputLines.join(EOL); 21 | return pipe( 22 | assertFileSystemObjectType(relativePath, [FileSystemObjectType.file, FileSystemObjectType.nothing]), 23 | chain(descriptor => async () => { 24 | if (descriptor.type === FileSystemObjectType.file) { 25 | // The ignore file already existed, so we will print a detailed explanation of changes to the file after writing it to disk, 26 | // to inform the end-user of all the changes made. 27 | const currentIgnoreFile = (await fs.promises.readFile(absolutePath)).toString(); 28 | const currentIgnoreFileLines = currentIgnoreFile.split(EOL).join("\n").split("\n"); 29 | const differences = deepComparison(currentIgnoreFileLines, outputLines, ""); 30 | if (differences.length > 0) { 31 | log.trace(`modifying file ${colorize.file(relativePath)}`); 32 | } 33 | differences.forEach(explanation => { 34 | log.info(explanation); 35 | }); 36 | } else if (descriptor.type === FileSystemObjectType.nothing) { 37 | log.info(`creating file ${colorize.file(relativePath)}`); 38 | } 39 | try { 40 | await fs.promises.writeFile(absolutePath, outputString); 41 | } catch(e) { 42 | throw new Error(e); 43 | } 44 | return right(outputString); 45 | }) 46 | ); 47 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/sync-logic/writers/json.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import { log } from '../../logging/log'; 5 | 6 | import { deepComparison } from '../deep-object-compare'; 7 | import { ConfigError } from '../../common/errors'; 8 | import { assertFileSystemObjectType } from '../../file-system/presence-assertions'; 9 | import { FileSystemObjectType } from '../../file-system/object'; 10 | import { colorize } from '../../colorize-special-text'; 11 | import { TaskEither, chain } from 'fp-ts/lib/TaskEither'; 12 | import { right } from 'fp-ts/lib/Either'; 13 | import { pipe } from 'fp-ts/lib/function'; 14 | 15 | export function writeJsonAndReportChanges(relativePath: string, outputObject: Object): TaskEither { 16 | const outputJSONString = JSON.stringify(outputObject, null, 2); 17 | const absolutePath = path.resolve(relativePath); 18 | return pipe( 19 | assertFileSystemObjectType(relativePath, [FileSystemObjectType.file, FileSystemObjectType.nothing]), 20 | chain(descriptor => async () => { 21 | if (descriptor.type === FileSystemObjectType.file) { 22 | // The json file already existed, so we will print a detailed explanation of changes to the file after writing it to disk, 23 | // to inform the end-user of all the changes made. 24 | const currentJSONString = (await fs.promises.readFile(absolutePath)).toString(); 25 | var parseFailed = false; 26 | var currentObject: any; 27 | try { 28 | currentObject = JSON.parse(currentJSONString); 29 | } catch(e) { 30 | parseFailed = true; 31 | log.error(`The current contents of '${colorize.file(relativePath)}' are not valid JSON.`); 32 | log.info(`Replacing current contents of '${colorize.file(relativePath)}' with the JSON\n${outputJSONString}`); 33 | } 34 | if (!parseFailed && currentObject !== undefined) { 35 | const differences = deepComparison(currentObject, outputObject, ""); 36 | if (differences.length > 0) { 37 | log.trace(`modifying file ${colorize.file(relativePath)}`); 38 | } 39 | differences.forEach(explanation => { 40 | log.info(explanation); 41 | }); 42 | } 43 | } else if (descriptor.type === FileSystemObjectType.nothing) { 44 | log.info(`creating file ${colorize.file(relativePath)}`); 45 | } 46 | try { 47 | await fs.promises.writeFile(absolutePath, outputJSONString); 48 | } catch(e) { 49 | throw new Error(e); 50 | } 51 | return right(outputJSONString); 52 | }) 53 | ); 54 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/ts-monorepo.ts: -------------------------------------------------------------------------------- 1 | import "source-map-support/register"; 2 | import { Worker, isMainThread, parentPort } from 'worker_threads'; 3 | import { log } from './logging/log'; 4 | import { restartProgram } from './process/restart-program'; 5 | import { syncMonorepo } from './sync-logic/sync-monorepo'; 6 | import { watch } from "./file-system/watcher"; 7 | import { CONFIG_FILE_NAME, CONFIG_FILE_ABSOLUTE_PATH, TOOL_SHORT_NAME, TOOL_VERSION } from './common/constants'; 8 | import { colorize } from "./colorize-special-text"; 9 | import { Terminateable } from "./common/types/traits"; 10 | import { tryCatch } from 'fp-ts/lib/TaskEither'; 11 | import { flatten, fold } from 'fp-ts/lib/Either'; 12 | import { pipe } from 'fp-ts/lib/function'; 13 | import { Option, some, none, isSome, isNone } from 'fp-ts/lib/Option'; 14 | import { ErrorType, ConfigError } from "./common/errors"; 15 | import { detectProgramChanges, initialize } from "./self-change-detector"; 16 | import { ChildToParentMessage, ParentToChildMessage } from "./process/parent-child-rpc"; 17 | 18 | const CONFIG_ERROR_LOGGING_ENABLED = true; 19 | const watchers: Terminateable[] = []; 20 | async function main() { 21 | log.info(`${colorize.package(TOOL_SHORT_NAME)} v${TOOL_VERSION}`); 22 | 23 | var updateQueued = false; // This ensures that only one sync monorepo operation is occurring at a time. 24 | var currentSyncTask = initialize(); // Could even be plain Promise.resolve(); doesn't matter really. Just needs to be a promise. 25 | var maybeActiveBuildTask: Option = none; 26 | function queueMonorepoSync() { 27 | if (updateQueued) return; 28 | updateQueued = true; 29 | currentSyncTask = currentSyncTask.then(async () => { 30 | updateQueued = false; 31 | if(isSome(maybeActiveBuildTask)) await maybeActiveBuildTask.value.terminate(); 32 | log.info(`Parsing ${colorize.file(CONFIG_FILE_NAME)}`); 33 | // TODO: extract port here. 34 | maybeActiveBuildTask = pipe( 35 | await tryCatch( 36 | syncMonorepo(), 37 | (e: any) => [{ 38 | type: ErrorType.UnexpectedRuntimeError, 39 | message: `${e.stack || e.message}` 40 | } as ConfigError] 41 | )(), 42 | flatten, 43 | thing => { 44 | return thing; 45 | }, 46 | fold( 47 | configErrors => { 48 | if (CONFIG_ERROR_LOGGING_ENABLED) { 49 | log.error(`${configErrors.length} errors:\n\n${ 50 | configErrors.map( 51 | (configError, index) => `${(index + 1)}. ${colorize.error(configError.type)}\n${configError.message}` 52 | ).join("\n\n") 53 | }\n`); 54 | } 55 | log.info("Waiting for changes..."); 56 | return none; 57 | }, 58 | some 59 | ) 60 | ); 61 | }); 62 | } 63 | 64 | watchers.push(await watch(CONFIG_FILE_ABSOLUTE_PATH, { 65 | onChange: queueMonorepoSync, 66 | onRemove() { 67 | log.warn(`${colorize.file(CONFIG_FILE_NAME)} deleted. Re-add it to resume watching.`); 68 | } 69 | })); 70 | 71 | 72 | function reportChanges(pastTenseVerb: string, files: string[]) { 73 | if (files.length === 0) return; 74 | log.info(`The following ${files.length} in-program file(s) were ${pastTenseVerb}:`); 75 | const leftPad = (files.length + "").length; 76 | files.forEach((file, index) => { 77 | log.info(` ${((index + 1) + "").padStart(leftPad, ' ')}. ${colorize.file(file)}`); 78 | }); 79 | } 80 | 81 | watchers.push(await watch(__filename, { 82 | async onChange() { 83 | const maybeChanges = await detectProgramChanges(); 84 | if (isNone(maybeChanges)) { 85 | log.info("Waiting for changes..."); 86 | return; 87 | } 88 | const changes = maybeChanges.value; 89 | restartProgram(async () => { 90 | reportChanges('added', changes.filesAdded); 91 | reportChanges('removed', changes.filesRemoved); 92 | reportChanges('modified', changes.filesChanged); 93 | reportChanges('resigned', changes.filesWithSignatureChanges); 94 | if (isSome(maybeActiveBuildTask)) await maybeActiveBuildTask.value.terminate(); 95 | log.info("Terminating watchers."); 96 | await Promise.all(watchers.map(watcher => watcher.terminate())); 97 | }); 98 | } 99 | })); 100 | 101 | queueMonorepoSync(); 102 | } 103 | 104 | if (isMainThread) { 105 | log.info(`pid = ${process.pid}`); 106 | log.info(`${colorize.subfeature("Master Thread")}: forking child thread which may be restarted if ${colorize.package(TOOL_SHORT_NAME)}'s own code changes`); 107 | function setupChild() { 108 | const worker = new Worker(__filename); 109 | var restarting = false; 110 | worker.on('message', messageBuffer => { 111 | const messageStr = messageBuffer.toString(); 112 | const message: ChildToParentMessage = JSON.parse(messageStr); 113 | if (message.type === 'restart') { 114 | log.info(`${colorize.subfeature("Master Thread")}: restarting child thread`); 115 | const toSend: ParentToChildMessage = { type: 'die' }; 116 | restarting = true; 117 | worker.postMessage(JSON.stringify(toSend)); 118 | } else { 119 | throw new Error(`Unknown message type from child thread ${message.type}`); 120 | } 121 | }); 122 | worker.on('exit', () => { 123 | if (restarting) { 124 | log.info(`${colorize.subfeature("Master Thread")}: forking new child thread`); 125 | setTimeout(() => setupChild(), 0); 126 | } else { 127 | log.info(`child exited without in non-restart condition. Exiting parent process.`); 128 | process.exit(); 129 | } 130 | }) 131 | } 132 | setupChild(); 133 | } else { 134 | main().catch(e => { 135 | log.info("Program crashed."); 136 | console.log(e.message || e.stack); 137 | }); 138 | parentPort!.on('message', messageBuffer => { 139 | const message: ParentToChildMessage = JSON.parse(messageBuffer.toString()); 140 | if (message.type === 'die') { 141 | process.exit(); 142 | } else { 143 | throw new Error(`Uknown message type from parent ${message.type}`); 144 | } 145 | }); 146 | } 147 | 148 | // This was experimental for trying to make this program a Yarn plugin. Ignore for now. 149 | export = { 150 | name: TOOL_SHORT_NAME, 151 | factory: (require: any) => { 152 | const { Command } = require('clipanion'); 153 | class ToolCommand extends Command { 154 | async execute() { 155 | try { 156 | await main(); 157 | } catch(e) { 158 | restartProgram(async () => { 159 | log.info("Program crashed."); 160 | console.log(e.message || e.stack); 161 | }); 162 | } 163 | } 164 | } 165 | ToolCommand.addPath(TOOL_SHORT_NAME); 166 | return { 167 | commands: [ ToolCommand as any ] 168 | }; 169 | } 170 | } -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/webpack/webpack-audit-hooks.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import * as webpack from 'webpack'; 3 | import { log } from "../logging/log"; 4 | import { colorize } from "../colorize-special-text"; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | const AUDIT_TAP_NAME = "AUDIT TAPPER"; 9 | 10 | //let remaining = 1; 11 | 12 | // Go to https://www.planttext.com/ to visualize output 13 | const FILE_PATH = path.resolve(`./bundle/build-at-${Date.now()}.txt`); 14 | const appendQueue: string[] = []; 15 | function append(line: string) { 16 | function write(outStream: fs.WriteStream) { 17 | const lineToWrite = appendQueue[0]; 18 | outStream.write(lineToWrite + "\n", error => { 19 | if (error) { 20 | log.error("unable to write line: " + lineToWrite); 21 | } 22 | appendQueue.shift(); 23 | if (appendQueue.length > 0) { 24 | write(outStream); 25 | } else { 26 | outStream.end(); 27 | } 28 | }); 29 | } 30 | appendQueue.push(line); 31 | if (appendQueue.length === 1) { 32 | const outStream = fs.createWriteStream(FILE_PATH, {flags: 'a'}); 33 | write(outStream); 34 | } 35 | } 36 | export function init() { 37 | append("@startuml"); 38 | append("digraph ABC {"); 39 | } 40 | export function close() { 41 | append("}"); 42 | append("@enduml"); 43 | } 44 | export function appendModule(module: webpack.Module, bad: boolean) { 45 | const { debugId, request, rawRequest, userRequest } = module; 46 | append(` m${debugId} [label="{req ${request} | raw ${rawRequest} | user ${userRequest}}"${bad ? ` fillcolor=red`: ""}]`); 47 | } 48 | export function appendRelationship(sourceModule: NormalModule, destModule: NormalModule, dependency: ModuleDependency) { 49 | append(` m${sourceModule.debugId} -> m${destModule.debugId} [label="${dependency.type}"]`); 50 | } 51 | 52 | export function setUpAudit(compiler: webpack.Compiler) { 53 | init 54 | appendModule 55 | appendRelationship 56 | //init(); 57 | compiler.hooks.compilation.tap(AUDIT_TAP_NAME, (compilation, _normalModuleFactory) => { 58 | log.trace('compiler.compilation'); 59 | compilation 60 | colorize 61 | let thing: ModuleGraphConnection = null as any; 62 | thing; 63 | // before module is built. 64 | /* 65 | compilation.hooks.buildModule.tap(AUDIT_TAP_NAME, ((module: FinalModule) => { 66 | //appendModule(module); 67 | const incoming: Set = (compilation as any).moduleGraph.getIncomingConnections(module); 68 | incoming.forEach(incomingConnection => { 69 | if (incomingConnection.originModule !== null) { 70 | appendRelationship(incomingConnection.originModule, incomingConnection.module, incomingConnection.dependency); 71 | } 72 | }); 73 | }) as any); 74 | 75 | compilation.hooks.failedModule.tap(AUDIT_TAP_NAME, ((module: FinalModule, error: Error) => { 76 | log.trace(`compiler.compilation.failedModule ${colorize.file(module.rawRequest)}. ${module.debugId}`); 77 | log.error('error with name ' + error.name) 78 | }) as any); 79 | 80 | compilation.hooks.succeedModule.tap(AUDIT_TAP_NAME, ((module: FinalModule) => { 81 | log.trace(`compiler.compilation.succeedModule ${colorize.file(module.rawRequest)}. ${module.debugId}`); 82 | }) as any); 83 | 84 | compilation.hooks.seal.tap(AUDIT_TAP_NAME, () => { 85 | log.trace('compiler.compilation.seal'); 86 | }); 87 | 88 | compilation.hooks.unseal.tap(AUDIT_TAP_NAME, () => { 89 | log.trace('compiler.compilation.unseal'); 90 | }); 91 | 92 | compilation.hooks.optimize.tap(AUDIT_TAP_NAME, () => { 93 | log.trace('compiler.compilation.optimize'); 94 | }); 95 | 96 | // SyncBailHook so we can bail by returning something. 97 | compilation.hooks.optimizeDependencies.tap(AUDIT_TAP_NAME, modules => { 98 | log.trace('compiler.compilation.optimizeDependencies'); 99 | log.info('total modules = ' + modules.length); 100 | }); 101 | 102 | compilation.hooks.afterOptimizeDependencies.tap(AUDIT_TAP_NAME, modules => { 103 | log.trace('compiler.compilation.afterOptimizeDependencies'); 104 | log.info("total modules = " + modules.length); 105 | }); 106 | 107 | // SyncBailHook 108 | compilation.hooks.optimizeModules.tap(AUDIT_TAP_NAME, modules => { 109 | log.trace('compiler.compilation.optimizeModules'); 110 | log.info('total modules = ' + modules.length); 111 | }); 112 | 113 | compilation.hooks.beforeModuleIds.tap(AUDIT_TAP_NAME, modules => { 114 | log.trace('compiler.compilation.beforeModuleIds'); 115 | log.info('total modules = ' + modules.length); 116 | }); 117 | 118 | compilation.hooks.moduleIds.tap(AUDIT_TAP_NAME, modules => { 119 | log.trace('compiler.compilation.moduleIds'); 120 | log.info('total modules = ' + modules.length); 121 | }); 122 | 123 | compilation.hooks.optimizeModuleIds.tap(AUDIT_TAP_NAME, modules => { 124 | log.trace('compiler.compilation.optimizeModuleIds'); 125 | log.info('total modules = ' + modules.length); 126 | }); 127 | 128 | compilation.hooks.afterOptimizeModuleIds.tap(AUDIT_TAP_NAME, modules => { 129 | log.trace('compiler.compilation.afterOptimizeModuleIds'); 130 | log.info('total modules = ' + modules.length); 131 | }); 132 | 133 | compilation.hooks.recordModules.tap(AUDIT_TAP_NAME, (modules, records) => { 134 | log.trace('compiler.compilation.recordModules'); 135 | log.info('total modules = ' + modules.length); 136 | log.info('total records = ' + records.length); 137 | }); 138 | }); 139 | 140 | compiler.hooks.done.tap(AUDIT_TAP_NAME, _stats => { 141 | //append("}"); 142 | //append("@enduml"); 143 | }) 144 | } 145 | */ -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/webpack/webpack-future-start.ts: -------------------------------------------------------------------------------- 1 | /* 2 | const compiler = webpack(generateWebpackConfig(TOOL_SHORT_NAME)); 3 | setUpAudit(compiler); 4 | compiler.run((error, stats) => { 5 | log.info("webpack finished"); 6 | 7 | if (error) { 8 | log.info("type of error is " + (typeof error)); 9 | log.info("error is"); 10 | log.error(error.stack || error.message); 11 | } else if (stats.hasErrors()) { 12 | log.error(`webpack has ${stats.compilation.errors.length} errors`); 13 | 14 | const seenModuleIds = new Set(); 15 | init(); 16 | function recurseUp(mod: NormalModule) { 17 | const incoming: Set = (stats.compilation as any).moduleGraph.getIncomingConnections(mod); 18 | incoming.forEach(incomingConnection => { 19 | const incomingMod = incomingConnection.originModule; 20 | if (incomingMod !== null) { 21 | appendRelationship(incomingMod, mod, incomingConnection.dependency); 22 | if (!seenModuleIds.has(incomingMod.debugId)) { 23 | seenModuleIds.add(mod.debugId); 24 | appendModule(incomingMod as FinalModule, false); 25 | recurseUp(incomingMod); 26 | } 27 | } 28 | }); 29 | } 30 | stats.compilation.errors.forEach((error: CompilationError, index) => { 31 | 32 | log.info(`${colorize.error((index + 1)+"")}. id = ${error.module.debugId}. Error = ${colorize.error(error.name)}`); 33 | console.log("\n"); 34 | console.log(error.error); 35 | console.log("\n"); 36 | 37 | const mod = error.module; 38 | seenModuleIds.add(mod.debugId); 39 | appendModule(mod, true); 40 | recurseUp(mod); 41 | }); 42 | close(); 43 | } else { 44 | log.info("There were no webpack errors"); 45 | } 46 | }) 47 | */ -------------------------------------------------------------------------------- /packages/@isomorphic-typescript/ts-monorepo~/source/webpack/webpack.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import * as webpack from 'webpack'; 3 | import * as path from 'path'; 4 | export const generateWebpackConfig = (outputName: string): webpack.Configuration => ({ 5 | mode: "none", 6 | output: { 7 | filename: `${outputName}.js`, 8 | path: path.resolve('./bundle') 9 | }, 10 | entry: './distribution/ts-monorepo.js', 11 | target: 'node', 12 | stats: 'verbose', 13 | recordsOutputPath: path.resolve('./bundle/records.json') 14 | }); 15 | */ -------------------------------------------------------------------------------- /prep-safe.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const SAFE_NAME = "dist-safe"; 5 | const SAFE_PACKAGE_JSON_PATH = path.resolve(`./${SAFE_NAME}/package.json`); 6 | const WORK_TREE_PACKAGE_JSON_PATH = path.resolve('./package.json'); 7 | 8 | async function main() { 9 | const safeJson = JSON.parse((await fs.promises.readFile(SAFE_PACKAGE_JSON_PATH)).toString()); 10 | safeJson.private = true; 11 | await fs.promises.writeFile(SAFE_PACKAGE_JSON_PATH, JSON.stringify(safeJson, null, 2)); 12 | const workTreeJson = JSON.parse((await fs.promises.readFile(WORK_TREE_PACKAGE_JSON_PATH)).toString()); 13 | workTreeJson.workspaces = [SAFE_NAME]; 14 | await fs.promises.writeFile(WORK_TREE_PACKAGE_JSON_PATH, JSON.stringify(workTreeJson, null, 2)); 15 | } 16 | 17 | main().then(() => { 18 | process.exit(); 19 | }); 20 | -------------------------------------------------------------------------------- /scripts/publish.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const child_process = require("child_process"); 4 | 5 | const stablePackageJSON = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../stable/package.json")).toString()); 6 | const version = stablePackageJSON.version; 7 | 8 | const [npm, filename, ...tags] = Array.from(process.argv.values()); 9 | 10 | tags.forEach(tag => { 11 | const command = `npm dist-tag add @isomorphic-typescript/ts-monorepo@${version} ${tag}`; 12 | console.log(command); 13 | child_process.execSync(command); 14 | }); -------------------------------------------------------------------------------- /scripts/untar.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const tar = require('tar'); 3 | 4 | const sourceTarLocation = process.argv[2]; 5 | const destinationDirectoryLocation = process.argv[3]; 6 | 7 | tar.extract({ 8 | cwd: path.resolve(destinationDirectoryLocation), 9 | file: path.resolve(sourceTarLocation), 10 | sync: true, 11 | strip: 1 12 | }); -------------------------------------------------------------------------------- /ts-monorepo.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.4.3", 3 | "ttypescript": false, // right now having some trouble with an error "Error: Debug Failure. False expression: File extension for signature expected to be dts" 4 | "port": 3000, 5 | "packages": { 6 | "@isomorphic-typescript": { 7 | "ts-monorepo~": { 8 | "extends": [], 9 | "files": { 10 | "json": { 11 | "package.json": { 12 | "author": "Alexander Leung", 13 | "license": "MIT", 14 | "description": "Yarn plugin for maintaining typescript monorepos", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/isomorphic-typescript/ts-monorepo.git" 18 | }, 19 | "bin": "./build/ts-monorepo.js", 20 | "devDependencies": [ 21 | "@types/node", 22 | "@types/comment-json", 23 | "@types/express", 24 | "@types/semver" 25 | ], 26 | "dependencies": [ 27 | "ansicolor", 28 | "chokidar", 29 | "comment-json", 30 | "deepmerge", 31 | "express", 32 | "fp-ts", 33 | "io-ts", 34 | "io-ts-types", 35 | "newtype-ts", 36 | "monocle-ts", 37 | "latest-version", 38 | "semver", 39 | "source-map-support", 40 | "webpack", 41 | "validate-npm-package-name", 42 | "ts-node", 43 | "typescript", 44 | // Not using this at the moment, but could be nice to transition away from io-ts for cleaner code in the future 45 | "typescript-is" 46 | ], 47 | "peerDependencies": [ 48 | "ttypescript" 49 | ], 50 | "keywords": [ 51 | "typescript", 52 | "lerna", 53 | "yarn", 54 | "berry", 55 | "webpack", 56 | "monorepo", 57 | "project", 58 | "references", 59 | "tsconfig", 60 | "tsconfig.json", 61 | "json", 62 | "incremental", 63 | "package.json" 64 | ] 65 | }, 66 | "tsconfig.json": { 67 | "compilerOptions": { 68 | "module": "commonjs", // needed for es6 target, not es5 target 69 | "target": "es6", 70 | 71 | /* Type Definitions */ 72 | "lib": ["esnext"], 73 | "types": ["node"], 74 | 75 | /* Code Quality */ 76 | "forceConsistentCasingInFileNames": true, 77 | "noImplicitReturns": true, 78 | "noUnusedLocals": true, 79 | "noUnusedParameters": true, 80 | "strict": true, // noImplicitAny noImplicitThis alwaysStrict strictBindCallApply strictFunctionTypes strictPropertyInitialization strictNullChecks 81 | "stripInternal": true, 82 | "plugins": [ 83 | { 84 | "transform": "typescript-is/lib/transform-inline/transformer" 85 | } 86 | ] 87 | } 88 | } 89 | }, 90 | "ignore": { 91 | ".npmignore": [ 92 | "source", 93 | "tsconfig.json", 94 | "tsconfig.tsbuildinfo" 95 | ] 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } --------------------------------------------------------------------------------