├── .codeclimate.yml ├── .editorconfig ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING ├── FUNDING.yml ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE └── workflows │ └── test.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── metapak.js ├── eslint.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── libs │ └── utils.ts └── services │ ├── assets.test.ts │ ├── assets.ts │ ├── fs.ts │ ├── gitHooks.test.ts │ ├── gitHooks.ts │ ├── metapak.test.ts │ ├── metapak.ts │ ├── packageConf.test.ts │ ├── packageConf.ts │ └── programOptions.ts └── tsconfig.json /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it elsewhere, changes would 3 | # be overridden. 4 | engines: 5 | eslint: 6 | enabled: true 7 | 8 | ratings: 9 | paths: 10 | - "'src/**/*.js' 'bin/**/*.js'" 11 | ## Exclude test files. 12 | exclude_patterns: 13 | - "dist/" 14 | - "**/node_modules/" 15 | - "src/**/*.mocha.js" 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it elsewhere, changes would 3 | # be overridden. 4 | 5 | # EditorConfig is awesome: http://EditorConfig.org 6 | 7 | # top-most EditorConfig file 8 | root = true 9 | 10 | # Unix-style newlines with a newline ending every file 11 | [*] 12 | end_of_line = lf 13 | insert_final_newline = true 14 | 15 | # Matches multiple files with brace expansion notation 16 | # Set default charset 17 | # 2 space indentation 18 | [*.{js,css}] 19 | charset = utf-8 20 | indent_style = space 21 | trim_trailing_whitespace = true 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Enforce Unix newlines 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Be kind, except if I behave like an asshole, if so, tell me by linking to this 4 | file. 5 | 6 | I try hard to document and automate things so that you cannot create noises 7 | without really willing to do so. 8 | 9 | This is why I'll just delete issues/comments making be sad. 10 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Contributing to this project requires you to be 2 | a gentleman. 3 | 4 | By contributing you must agree with publishing your 5 | changes into the same license that apply to the current code. 6 | 7 | You will find the license in the LICENSE file at 8 | the root of this repository. 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [nfroidure] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## Issue 2 | 16 | 17 | I'm a gentledev I: 18 | - [ ] fully read the README recently 19 | - [ ] searched for existing issues 20 | - [ ] checked I'm up to date with the latest version of the project 21 | 22 | ### Expected behavior 23 | 24 | ### Actual behavior 25 | 26 | ### Steps to reproduce the behavior 27 | 28 | ### Debugging informations 29 | - `node -v` result: 30 | ``` 31 | 32 | ``` 33 | 34 | - `npm -v` result: 35 | ``` 36 | 37 | ``` 38 | If the result is lower than 20.11.1, there is 39 | poor chances I even have a look to it. Please, 40 | use the last [NodeJS LTS version](https://nodejs.org/en/). 41 | 42 | ## Feature request 43 | 54 | 55 | ### Feature description 56 | 57 | ### Use cases 58 | 59 | - [ ] I will/did implement the feature 60 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | 10 | 11 | Fixes # 12 | 13 | ### Proposed changes 14 | - 15 | - 16 | 17 | 18 | 19 | ### Code quality 20 | - [ ] I made some tests for my changes 21 | - [ ] I added my name in the 22 | [contributors](https://docs.npmjs.com/files/package.json#people-fields-author-contributors) 23 | field of the `package.json` file. Beware to use the same format than for the author field 24 | for the entries so that you'll get a mention in the `README.md` with a link to your website. 25 | 26 | ### License 27 | To get your contribution merged, you must check the following. 28 | 29 | - [ ] I read the project license in the LICENSE file 30 | - [ ] I agree with publishing under this project license 31 | 32 | 46 | ### Join 47 | - [ ] I wish to join the core team 48 | - [ ] I agree that with great powers comes responsibilities 49 | - [ ] I'm a nice person 50 | 51 | My NPM username: 52 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it here, your changes would 3 | # be overridden. 4 | 5 | name: Node.js CI 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: [main] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | - name: Install dependencies 29 | run: npm ci 30 | - name: Run pre-commit tests 31 | run: npm run precz 32 | - name: Run coverage 33 | run: npm run cover 34 | - name: Report Coveralls 35 | uses: coverallsapp/github-action@v2 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by a `metapak` 2 | # module. Do not change it elsewhere, changes would 3 | # be overridden. 4 | 5 | # Created by https://www.gitignore.io/api/osx,node,linux 6 | 7 | ### Linux ### 8 | *~ 9 | 10 | # temporary files which can be created if a process still has a handle open of a deleted file 11 | .fuse_hidden* 12 | 13 | # KDE directory preferences 14 | .directory 15 | 16 | # Linux trash folder which might appear on any partition or disk 17 | .Trash-* 18 | 19 | # .nfs files are created when an open file is removed but is still being accessed 20 | .nfs* 21 | 22 | ### Node ### 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | bower_components 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (https://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | node_modules/ 59 | jspm_packages/ 60 | 61 | # TypeScript v1 declaration files 62 | typings/ 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # next.js build output 86 | .next 87 | 88 | # nuxt.js build output 89 | .nuxt 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless 96 | 97 | ### OSX ### 98 | # General 99 | .DS_Store 100 | .AppleDouble 101 | .LSOverride 102 | 103 | # Icon must end with two \r 104 | Icon 105 | 106 | # Thumbnails 107 | ._* 108 | 109 | # Files that might appear in the root of a volume 110 | .DocumentRevisions-V100 111 | .fseventsd 112 | .Spotlight-V100 113 | .TemporaryItems 114 | .Trashes 115 | .VolumeIcon.icns 116 | .com.apple.timemachine.donotpresent 117 | 118 | # Directories potentially created on remote AFP share 119 | .AppleDB 120 | .AppleDesktop 121 | Network Trash Folder 122 | Temporary Items 123 | .apdisk 124 | 125 | 126 | # End of https://www.gitignore.io/api/osx,node,linux 127 | 128 | # Coveralls key 129 | .coveralls.yml 130 | 131 | # Project custom ignored file 132 | dist 133 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "gruntfuggly.todo-tree" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [6.0.2](https://github.com/nfroidure/metapak/compare/v6.0.1...v6.0.2) (2024-12-04) 2 | 3 | 4 | 5 | ## [6.0.1](https://github.com/nfroidure/metapak/compare/v6.0.0...v6.0.1) (2024-07-15) 6 | 7 | 8 | 9 | # [6.0.0](https://github.com/nfroidure/metapak/compare/v5.1.8...v6.0.0) (2024-02-27) 10 | 11 | 12 | 13 | ## [5.1.8](https://github.com/nfroidure/metapak/compare/v5.1.7...v5.1.8) (2024-02-24) 14 | 15 | 16 | 17 | ## [5.1.7](https://github.com/nfroidure/metapak/compare/v5.1.6...v5.1.7) (2023-08-20) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * **glob:** fix glob usage ([017e4cd](https://github.com/nfroidure/metapak/commit/017e4cd361d61f33a37d754859cff20cd2914c8f)) 23 | 24 | 25 | 26 | ## [5.1.6](https://github.com/nfroidure/metapak/compare/v5.1.5...v5.1.6) (2023-08-16) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **types:** remove not necessary types packages ([1142dbf](https://github.com/nfroidure/metapak/commit/1142dbfba709264520a32688b93375f7922d646f)) 32 | 33 | 34 | 35 | ## [5.1.5](https://github.com/nfroidure/metapak/compare/v5.1.4...v5.1.5) (2023-08-16) 36 | 37 | 38 | 39 | ## [5.1.4](https://github.com/nfroidure/metapak/compare/v5.1.3...v5.1.4) (2023-08-16) 40 | 41 | 42 | 43 | ## [5.1.3](https://github.com/nfroidure/metapak/compare/v5.1.2...v5.1.3) (2023-08-11) 44 | 45 | 46 | 47 | ## [5.1.2](https://github.com/nfroidure/metapak/compare/v5.1.1...v5.1.2) (2023-08-11) 48 | 49 | 50 | 51 | ## [5.1.1](https://github.com/nfroidure/metapak/compare/v5.1.0...v5.1.1) (2023-08-11) 52 | 53 | 54 | 55 | # [5.1.0](https://github.com/nfroidure/metapak/compare/v5.0.1...v5.1.0) (2023-08-07) 56 | 57 | 58 | 59 | ## [5.0.1](https://github.com/nfroidure/metapak/compare/v5.0.0...v5.0.1) (2023-05-28) 60 | 61 | 62 | 63 | # [5.0.0](https://github.com/nfroidure/metapak/compare/v4.1.0...v5.0.0) (2023-05-27) 64 | 65 | 66 | 67 | # [4.1.0](https://github.com/nfroidure/metapak/compare/v4.0.6...v4.1.0) (2023-03-09) 68 | 69 | 70 | 71 | ## [4.0.6](https://github.com/nfroidure/metapak/compare/v4.0.5...v4.0.6) (2023-01-03) 72 | 73 | 74 | ### Features 75 | 76 | * **build:** allow to use built metapak modules ([b614983](https://github.com/nfroidure/metapak/commit/b61498319e642d42fd840bf987bab0591c10d5f0)) 77 | * **types:** add more typing and export it ([8011ef2](https://github.com/nfroidure/metapak/commit/8011ef2a34232eed377fa4e56ffcc5635e94e41e)) 78 | 79 | 80 | 81 | ## [4.0.5](https://github.com/nfroidure/metapak/compare/v4.0.4...v4.0.5) (2023-01-01) 82 | 83 | 84 | 85 | ## [4.0.4](https://github.com/nfroidure/metapak/compare/v4.0.3...v4.0.4) (2022-05-26) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * **options:** fix CLI options ([6f5fc2f](https://github.com/nfroidure/metapak/commit/6f5fc2fb90580668bbf97f5b5143de02b6ee8594)) 91 | 92 | 93 | 94 | ## [4.0.3](https://github.com/nfroidure/metapak/compare/v4.0.2...v4.0.3) (2022-05-25) 95 | 96 | 97 | 98 | ## [4.0.2](https://github.com/nfroidure/metapak/compare/v4.0.1...v4.0.2) (2021-04-09) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * **require:** add forgotten require default ([add7c4e](https://github.com/nfroidure/metapak/commit/add7c4e71fabac3d191440bd23ba98ae21c13d10)) 104 | 105 | 106 | 107 | ## [4.0.1](https://github.com/nfroidure/metapak/compare/v4.0.0...v4.0.1) (2021-04-09) 108 | 109 | 110 | 111 | # [4.0.0](https://github.com/nfroidure/metapak/compare/v3.1.10...v4.0.0) (2021-04-09) 112 | 113 | 114 | ### chore 115 | 116 | * **dependencies:** update dependencies ([c5ed8e2](https://github.com/nfroidure/metapak/commit/c5ed8e2f4bcd713f6032ff3f7d9a0e9d8e16c1bd)) 117 | 118 | 119 | ### BREAKING CHANGES 120 | 121 | * **dependencies:** Only work with Node12+ 122 | 123 | 124 | 125 | ## [3.1.10](https://github.com/nfroidure/metapak/compare/v3.1.9...v3.1.10) (2020-05-17) 126 | 127 | 128 | 129 | ## [3.1.9](https://github.com/nfroidure/metapak/compare/v3.1.8...v3.1.9) (2020-05-17) 130 | 131 | 132 | 133 | ## [3.1.8](https://github.com/nfroidure/metapak/compare/v3.1.7...v3.1.8) (2020-03-20) 134 | 135 | 136 | ### Bug Fixes 137 | 138 | * **dependencies:** fix mkdirp signarutre change ([921f0cb](https://github.com/nfroidure/metapak/commit/921f0cbe027c697e94c07d3e7f5710ef1da61dc2)) 139 | 140 | 141 | 142 | ## [3.1.7](https://github.com/nfroidure/metapak/compare/v3.1.6...v3.1.7) (2020-02-02) 143 | 144 | 145 | 146 | ## [3.1.6](https://github.com/nfroidure/metapak/compare/v3.1.5...v3.1.6) (2019-02-13) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * **Core:** Fix metapak module check for plugins running themselves ([6a71d23](https://github.com/nfroidure/metapak/commit/6a71d23)) 152 | 153 | 154 | 155 | ## [3.1.5](https://github.com/nfroidure/metapak/compare/v3.1.4...v3.1.5) (2019-02-03) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * **Plugin resolution:** Use the `require` resolution to retrieve configs ([87946c2](https://github.com/nfroidure/metapak/commit/87946c2)) 161 | 162 | 163 | 164 | ## [3.1.4](https://github.com/nfroidure/metapak/compare/v3.1.3...v3.1.4) (2019-02-03) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * **Install:** Remove the install script which in not useful ([fab9d59](https://github.com/nfroidure/metapak/commit/fab9d59)) 170 | 171 | 172 | 173 | ## [3.1.3](https://github.com/nfroidure/metapak/compare/v3.1.2...v3.1.3) (2019-02-03) 174 | 175 | 176 | 177 | ## [3.1.2](https://github.com/nfroidure/metapak/compare/v3.1.1...v3.1.2) (2019-02-02) 178 | 179 | 180 | 181 | ## [3.1.1](https://github.com/nfroidure/metapak/compare/v3.1.0...v3.1.1) (2019-01-28) 182 | 183 | 184 | ### Bug Fixes 185 | 186 | * **Core:** Avoid false positive warnings ([e609347](https://github.com/nfroidure/metapak/commit/e609347)) 187 | 188 | 189 | 190 | # [3.1.0](https://github.com/nfroidure/metapak/compare/v3.0.1...v3.1.0) (2019-01-27) 191 | 192 | 193 | 194 | ## [3.0.1](https://github.com/nfroidure/metapak/compare/v3.0.0...v3.0.1) (2019-01-27) 195 | 196 | 197 | ### Bug Fixes 198 | 199 | * **Metapak config:** Add forgotten bundle files ([7aa4c79](https://github.com/nfroidure/metapak/commit/7aa4c79)) 200 | 201 | 202 | 203 | # [3.0.0](https://github.com/nfroidure/metapak/compare/v2.0.0...v3.0.0) (2019-01-27) 204 | 205 | 206 | ### Features 207 | 208 | * **Hooks:** Avoid running hooks transforms for non-root repos ([ccfe02b](https://github.com/nfroidure/metapak/commit/ccfe02b)) 209 | 210 | 211 | ### BREAKING CHANGES 212 | 213 | * **Hooks:** Will break for versions prior to Node 8 214 | 215 | 216 | 217 | 218 | # [2.0.0](https://github.com/nfroidure/metapak/compare/v1.0.3...v2.0.0) (2018-10-21) 219 | 220 | 221 | ### Bug Fixes 222 | 223 | * **bin/metapak.js:** fix .git path ([82bed36](https://github.com/nfroidure/metapak/commit/82bed36)) 224 | 225 | 226 | 227 | 228 | ## [1.0.3](https://github.com/nfroidure/metapak/compare/v1.0.2...v1.0.3) (2018-02-06) 229 | 230 | 231 | ### Bug Fixes 232 | 233 | * **metapak.js:** Accept scoped packages ([65f001f](https://github.com/nfroidure/metapak/commit/65f001f)) 234 | 235 | 236 | 237 | 238 | ## [1.0.2](https://github.com/nfroidure/metapak/compare/v1.0.1...v1.0.2) (2017-12-03) 239 | 240 | 241 | ### Bug Fixes 242 | 243 | * **Bin:** Print catched errors in stderr ([12bc76c](https://github.com/nfroidure/metapak/commit/12bc76c)) 244 | * **Install:** Fix post install script ([8b385c5](https://github.com/nfroidure/metapak/commit/8b385c5)) 245 | 246 | 247 | 248 | 249 | ## [1.0.1](https://github.com/nfroidure/metapak/compare/v1.0.0...v1.0.1) (2017-12-02) 250 | 251 | 252 | ### Bug Fixes 253 | 254 | * **Configs:** Preserve original configs sequence ([984f830](https://github.com/nfroidure/metapak/commit/984f830)) 255 | * **Dependencies:** Update debug due to its vulnerability ([28f24de](https://github.com/nfroidure/metapak/commit/28f24de)) 256 | 257 | 258 | 259 | 260 | # [1.0.0](https://github.com/nfroidure/metapak/compare/v0.0.21...v1.0.0) (2017-11-26) 261 | 262 | 263 | ### Bug Fixes 264 | 265 | * **Install:** No more automatic metapak run ([3b74e80](https://github.com/nfroidure/metapak/commit/3b74e80)), closes [#11](https://github.com/nfroidure/metapak/issues/11) [#3](https://github.com/nfroidure/metapak/issues/3) 266 | 267 | 268 | ### Features 269 | 270 | * **Assets:** Rename `_dot_` prefixed files ([9dd73f7](https://github.com/nfroidure/metapak/commit/9dd73f7)), closes [#4](https://github.com/nfroidure/metapak/issues/4) 271 | * **Bin:** Add options for dry and safe runs ([db78e64](https://github.com/nfroidure/metapak/commit/db78e64)) 272 | * **Package:** Warn users that using `metapak` to chenges dependencies is not nice. ([79ce7e8](https://github.com/nfroidure/metapak/commit/79ce7e8)), closes [#9](https://github.com/nfroidure/metapak/issues/9) 273 | 274 | 275 | ### BREAKING CHANGES 276 | 277 | * **Install:** Break previous metapak configurations 278 | 279 | 280 | 281 | 282 | ## [0.0.21](https://github.com/nfroidure/metapak/compare/v0.0.20...v0.0.21) (2017-07-15) 283 | 284 | 285 | 286 | 287 | ## [0.0.20](https://github.com/nfroidure/metapak/compare/v0.0.19...v0.0.20) (2017-04-14) 288 | 289 | 290 | ### Bug Fixes 291 | 292 | * **hooks:** Fix the git hooks directory retrieval on MacOSX ([85f0d1d](https://github.com/nfroidure/metapak/commit/85f0d1d)) 293 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2017 Nicolas Froidure 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [//]: # ( ) 2 | [//]: # (This file is automatically generated by a `metapak`) 3 | [//]: # (module. Do not change it except between the) 4 | [//]: # (`content:start/end` flags, your changes would) 5 | [//]: # (be overridden.) 6 | [//]: # ( ) 7 | # metapak 8 | > Node modules authoring made easy. 9 | 10 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nfroidure/metapak/blob/main/LICENSE) 11 | [![Coverage Status](https://coveralls.io/repos/github/nfroidure/metapak/badge.svg?branch=main)](https://coveralls.io/github/nfroidure/metapak?branch=main) 12 | 13 | 14 | [//]: # (::contents:start) 15 | 16 | ## What's that? 17 | 18 | `metapak` provides a set of tools to build your own meta npm packages easily. 19 | 20 | A meta npm package takes advantage of npm 21 | [lifecycle scripts](https://docs.npmjs.com/misc/scripts) to allow you to manage 22 | several similar npm packages/NodeJS projects in a simple and versioned way. 23 | 24 | Here is a [simple deck](https://slides.com/nfroidure/meta-npm-packages/live#/) 25 | introducing it. 26 | 27 | ## What is it good for? 28 | 29 | Let's say you are the author of thousands of Node modules. Now, imagine you 30 | want, for all of them: 31 | 32 | - change your linter, 33 | - change your license, 34 | - change your CI provider, 35 | - add a README template system, 36 | - add a contributors guide, 37 | - setup git hooks. 38 | 39 | This could look like a developer nightmare but with `metapak` you can manage 40 | that complexity by creating idempotent scripts to run on all your projects. 41 | 42 | ## Features 43 | 44 | Allows you to create a npm meta module to: 45 | 46 | - amend all your npm modules `package.json` globally, in a composable way 47 | (shared dependencies, utility scripts etc...), 48 | - add assets to all your projects without polluting your git history with 49 | insignificant changes, 50 | - automatically install git hooks so that all your coding flow are respected by 51 | your contributors. 52 | 53 | `metapak` can handle several meta packages so that you can compose them easily 54 | and keep them small and focused on one concern. 55 | 56 | Zero config for your contributors, nothing to install globally. 57 | 58 | ## Usage 59 | 60 | First create your own `metapak` module (you can look 61 | [at mine](https://github.com/nfroidure/metapak-nfroidure) to grasp its 62 | architecture). 63 | 64 | You **must** name your module with the `metapak-` prefix in order to make it 65 | work. 66 | 67 | Now, just define a configuration (named `main` here) you will be able to apply 68 | to all your NPM modules: 69 | 70 | ```sh 71 | mkdir src 72 | mkdir src/main 73 | 74 | # Let's set the package.json of your modules 75 | # Note this has to be an idempotent function 76 | # (ie: same run same result) 77 | echo " 78 | module.exports = (packageConf) => { 79 | // Looks like i am the contributor of all 80 | // my modules ;) 81 | packageConf.author = 'Nicolas Froidure'; 82 | 83 | // I mostly publish under MIT license, 84 | // let's default to it 85 | packageConf.license = 'MIT'; 86 | 87 | // Let's add my handy scripts 88 | packageConf.scripts = packageConf.scripts || {}; 89 | packageConf.scripts.lint = 'eslint'; 90 | 91 | // And the MUST HAVE dependencies 92 | packageConf.dependencies = packageConf.dependencies || {}; 93 | packageConf.dependencies.debug = '1.0.0'; 94 | 95 | // And the MUST HAVE dev dependencies 96 | packageConf.devDependencies = packageConf.devDependencies || {}; 97 | packageConf.devDependencies.eslint = '3.0.0'; 98 | 99 | return packageConf; 100 | }" > src/main/package.js 101 | 102 | # Let's also add some common assets 103 | # metapak will add/update for us 104 | mkdir src/main/assets 105 | # Adding the license 106 | wget -O src/main/assets/LICENSE https://mit-license.org/license.txt 107 | # Adding a git ignore file 108 | # Note we replaced the dot of the file per _dot_ 109 | # This is due to a magic behavior of npm 110 | # See: https://github.com/npm/npm/issues/15660 111 | # metapak will rename it to .gitignore 112 | echo "node_modules" > src/main/assets/_dot_gitignore 113 | 114 | # And make some additions to them, like templating 115 | echo " 116 | module.exports = (file, packageConf) => { 117 | // Simple templating of the LICENSE 118 | // There is no glob matching or templating system 119 | // in metapak to let you choose the ones you like 120 | if(file.name === 'LICENSE') { 121 | file.data = file.data.replace( 122 | //g, 123 | 'Nicolas Froidure' 124 | ); 125 | return file; 126 | } 127 | return file; 128 | }; 129 | " > src/main/assets.js 130 | 131 | # Finally let's add my git hooks on it 132 | echo "module.exports = (hooks, packageConf) => { 133 | hooks['pre-commit'] = hooks['pre-commit'] || []; 134 | 135 | // Ensure tests and linting are ok 136 | hooks['pre-commit'].push('npm run test && npm run lint || exit 1'); 137 | 138 | // Ensure that metapak state is stable 139 | // Indeed, you do not want to commit 140 | // while metapak has some changes to do 141 | // doing so would create a gap between 142 | // you metapak module/config and the 143 | // repository contents 144 | hooks['pre-commit'].push('npm run metapak -- --safe || exit 1'); 145 | return hooks; 146 | }; 147 | " > src/main/hooks.js 148 | ``` 149 | 150 | For convenience, you can add a peer dependency to your metapak plugin to force a 151 | given metapak version: 152 | 153 | ```json 154 | { 155 | "peerDependencies": { 156 | "metapak": "^4.0.4" 157 | } 158 | } 159 | ``` 160 | 161 | Now publish your package to npm and install it in all your repositories 162 | development dependencies with metapak: 163 | 164 | ``` 165 | npm i --save-dev metapak metapak-nfroidure 166 | ``` 167 | 168 | And declare the configuration to apply it: 169 | 170 | ```json 171 | { 172 | "version": "1.0.0", 173 | "metapak": { 174 | "configs": ["main"] 175 | } 176 | "scripts": { 177 | "metapak": "metapak" 178 | } 179 | } 180 | ``` 181 | 182 | Now by running: 183 | 184 | ```sh 185 | npm run metapak 186 | ``` 187 | 188 | All changes will apply automatically. If you are in a CI/CD context, you will 189 | take benefit to use `npm run metapak -- --safe` that will make the command fail 190 | if there is any change. It is useful to avoid commit unstable changes. 191 | 192 | That's it! There is a lot of things you can set on all your projects like CI 193 | scripts, linters, tests configuration etc... 194 | 195 | You can also create specific configs and combine them. Let's say I work for the 196 | Big Brother inc. and i want to add special behaviors for the modules I create at 197 | work: 198 | 199 | ```sh 200 | mkdir src/bigbrother 201 | 202 | # Let's add a package.json template 203 | echo " 204 | module.exports = (packageConf) => { 205 | // Lets proudly claim I work at BB inc.! 206 | packageConf.author = 'Nicolas Froidure (Big Brother inc.)'; 207 | 208 | // Let's change the license 209 | packageConf.license = 'SEE LICENSE IN LICENSE.md'; 210 | 211 | // Let's avoid loosing my job :D 212 | packageConf.private = true; 213 | 214 | return packageConf; 215 | }" > src/bigbrother/package.js 216 | 217 | # Simply override the default license 218 | mkdir src/bigbrother/assets 219 | echo " 220 | Copyright Big Brother inc. All rights reserved. 221 | " > src/bigbrother/assets/LICENSE.md 222 | ``` 223 | 224 | Now, just create a new version of your package, publish it and add this specific 225 | behavior by adding the following property to your Big Brother's projects: 226 | 227 | ``` 228 | { 229 | "version": "1.0.0", 230 | "metapak": { 231 | "configs": ["main", "bigbrother"] 232 | } 233 | } 234 | ``` 235 | 236 | **Note:** You can use a built project for your `metapak` module but in this 237 | case, you will have to use the `dist` folder instead of the `src` one to put 238 | your configs. Assets remain in the `src` one so do not forget to bundle the 239 | `src` folder into your final NPM module. 240 | 241 | ## Contributing 242 | 243 | To contribute to Metapak, simply clone this repository and run the tests. To 244 | test the CLI, use: 245 | 246 | ```sh 247 | node bin/metapak.js 248 | ``` 249 | 250 | [//]: # (::contents:end) 251 | 252 | # Authors 253 | - [Nicolas Froidure](http://insertafter.com/en/index.html) 254 | 255 | # License 256 | [MIT](https://github.com/nfroidure/metapak/blob/main/LICENSE) 257 | -------------------------------------------------------------------------------- /bin/metapak.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import { runMetapak } from '../dist/index.js'; 4 | 5 | runMetapak(); 6 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // This file is automatically generated by a `metapak` 3 | // module. Do not change it elsewhere, changes would 4 | // be overridden. 5 | 6 | import eslint from '@eslint/js'; 7 | import tseslint from 'typescript-eslint'; 8 | import eslintConfigPrettier from 'eslint-config-prettier'; 9 | import eslintPluginJest from 'eslint-plugin-jest'; 10 | 11 | export default tseslint.config( 12 | { 13 | files: ['**/*.ts'], 14 | ignores: ['**/*.d.ts'], 15 | extends: [ 16 | eslint.configs.recommended, 17 | ...tseslint.configs.recommended, 18 | ], 19 | }, 20 | { 21 | files: ['*.test.ts'], 22 | ...eslintPluginJest.configs['flat/recommended'], 23 | }, 24 | eslintConfigPrettier, 25 | { 26 | name: 'Project config', 27 | languageOptions: { 28 | ecmaVersion: 2018, 29 | sourceType: 'module', 30 | }, 31 | ignores: ['*.d.ts'], 32 | }, 33 | ); 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "metapak": { 3 | "configs": [ 4 | "main", 5 | "readme", 6 | "tsesm", 7 | "eslint", 8 | "jest", 9 | "ghactions", 10 | "coveralls" 11 | ], 12 | "data": { 13 | "files": "'src/**/*.ts'", 14 | "testsFiles": "'src/**/*.test.ts'", 15 | "distFiles": "'dist/**/*.js'", 16 | "ignore": [ 17 | "dist" 18 | ], 19 | "bundleFiles": [ 20 | "dist", 21 | "src", 22 | "bin" 23 | ] 24 | } 25 | }, 26 | "name": "metapak", 27 | "version": "6.0.2", 28 | "description": "Node modules authoring made easy.", 29 | "type": "module", 30 | "main": "dist/index.js", 31 | "types": "dist/index.d.ts", 32 | "bin": { 33 | "metapak": "bin/metapak.js" 34 | }, 35 | "scripts": { 36 | "build": "rimraf 'dist' && tsc --outDir dist", 37 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md", 38 | "cli": "env NODE_ENV=${NODE_ENV:-cli}", 39 | "cover": "npm run jest -- --coverage", 40 | "cz": "env NODE_ENV=${NODE_ENV:-cli} git cz", 41 | "format": "npm run prettier", 42 | "jest": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest", 43 | "lint": "eslint 'src/**/*.ts'", 44 | "metapak": "node bin/metapak.js", 45 | "mocha": "mocha 'src/**/*.mocha.js'", 46 | "precz": "npm run build && npm t && npm run lint && npm run metapak -- -s", 47 | "prettier": "prettier --write 'src/**/*.ts'", 48 | "preversion": "npm run build && npm t && npm run lint && npm run metapak -- -s", 49 | "rebuild": "swc ./src -s -d dist -C jsc.target=es2022", 50 | "test": "npm run jest", 51 | "type-check": "tsc --pretty --noEmit", 52 | "version": "npm run changelog" 53 | }, 54 | "repository": { 55 | "type": "git", 56 | "url": "git+https://github.com/nfroidure/metapak.git" 57 | }, 58 | "author": { 59 | "name": "Nicolas Froidure", 60 | "email": "nicolas.froidure@insertafter.com", 61 | "url": "http://insertafter.com/en/index.html" 62 | }, 63 | "license": "MIT", 64 | "bugs": { 65 | "url": "https://github.com/nfroidure/metapak/issues" 66 | }, 67 | "homepage": "https://github.com/nfroidure/metapak#readme", 68 | "dependencies": { 69 | "application-services": "^7.0.0", 70 | "chalk": "^5.3.0", 71 | "commander": "^12.1.0", 72 | "common-services": "^17.0.0", 73 | "debug": "^4.3.7", 74 | "diff": "^7.0.0", 75 | "glob": "^11.0.0", 76 | "knifecycle": "^18.0.0", 77 | "mkdirp": "^3.0.1", 78 | "pkg-dir": "^8.0.0", 79 | "sort-keys": "^5.1.0", 80 | "type-fest": "^4.30.0", 81 | "yerror": "^8.0.0" 82 | }, 83 | "devDependencies": { 84 | "@eslint/js": "^9.16.0", 85 | "@swc/cli": "^0.5.2", 86 | "@swc/core": "^1.10.0", 87 | "@swc/helpers": "^0.5.15", 88 | "@swc/jest": "^0.2.37", 89 | "commitizen": "^4.3.1", 90 | "conventional-changelog-cli": "^5.0.0", 91 | "cz-conventional-changelog": "^3.3.0", 92 | "eslint": "^9.16.0", 93 | "eslint-config-prettier": "^9.1.0", 94 | "eslint-plugin-jest": "^28.9.0", 95 | "eslint-plugin-prettier": "^5.2.1", 96 | "jest": "^29.7.0", 97 | "metapak-nfroidure": "19.0.0", 98 | "prettier": "^3.4.2", 99 | "rimraf": "^6.0.1", 100 | "typescript": "^5.7.2", 101 | "typescript-eslint": "^8.17.0" 102 | }, 103 | "engines": { 104 | "node": ">=20.11.1" 105 | }, 106 | "config": { 107 | "commitizen": { 108 | "path": "./node_modules/cz-conventional-changelog" 109 | } 110 | }, 111 | "contributors": [], 112 | "files": [ 113 | "dist", 114 | "src", 115 | "bin", 116 | "LICENSE", 117 | "README.md", 118 | "CHANGELOG.md" 119 | ], 120 | "greenkeeper": { 121 | "ignore": [ 122 | "commitizen", 123 | "cz-conventional-changelog", 124 | "conventional-changelog-cli", 125 | "typescript", 126 | "rimraf", 127 | "@swc/cli", 128 | "@swc/core", 129 | "@swc/helpers", 130 | "eslint", 131 | "prettier", 132 | "eslint-config-prettier", 133 | "eslint-plugin-prettier", 134 | "typescript-eslint", 135 | "jest", 136 | "@swc/jest" 137 | ] 138 | }, 139 | "prettier": { 140 | "semi": true, 141 | "printWidth": 80, 142 | "singleQuote": true, 143 | "trailingComma": "all", 144 | "proseWrap": "always" 145 | }, 146 | "jest": { 147 | "coverageReporters": [ 148 | "lcov" 149 | ], 150 | "testPathIgnorePatterns": [ 151 | "/node_modules/" 152 | ], 153 | "roots": [ 154 | "/src" 155 | ], 156 | "transform": { 157 | "^.+\\.tsx?$": [ 158 | "@swc/jest", 159 | {} 160 | ] 161 | }, 162 | "testEnvironment": "node", 163 | "moduleNameMapper": { 164 | "#(.*)": "/node_modules/$1", 165 | "(.+)\\.js": "$1" 166 | }, 167 | "extensionsToTreatAsEsm": [ 168 | ".ts" 169 | ], 170 | "prettierPath": null 171 | }, 172 | "overrides": { 173 | "eslint": "^9.16.0" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Knifecycle, constant, autoService, name } from 'knifecycle'; 2 | import { initLog, initImporter, initResolve } from 'common-services'; 3 | import initDebug from 'debug'; 4 | import os from 'node:os'; 5 | import { join, isAbsolute } from 'node:path'; 6 | import { env, exit } from 'node:process'; 7 | import { glob } from 'glob'; 8 | import { exec } from 'node:child_process'; 9 | import initFS from './services/fs.js'; 10 | import initMetapak from './services/metapak.js'; 11 | import initBuildPackageConf from './services/packageConf.js'; 12 | import initBuildPackageAssets from './services/assets.js'; 13 | import initBuildPackageGitHooks from './services/gitHooks.js'; 14 | import { initProjectDir } from 'application-services'; 15 | import initProgramOptions from './services/programOptions.js'; 16 | import type { MetapakService } from './services/metapak.js'; 17 | import type { 18 | MetapakPackageJson, 19 | PackageJSONTransformer, 20 | } from './libs/utils.js'; 21 | import type { PackageAssetsTransformer } from './services/assets.js'; 22 | import type { GitHooksTransformer } from './services/gitHooks.js'; 23 | import type { FSService } from './services/fs.js'; 24 | import type { LogService } from 'common-services'; 25 | 26 | export type { 27 | MetapakPackageJson, 28 | PackageAssetsTransformer, 29 | PackageJSONTransformer, 30 | GitHooksTransformer, 31 | FSService, 32 | LogService, 33 | }; 34 | 35 | export async function runMetapak() { 36 | try { 37 | const $ = await prepareMetapak(); 38 | 39 | const { metapak } = await $.run<{ 40 | metapak: MetapakService; 41 | }>(['metapak']); 42 | 43 | await metapak(); 44 | } catch (err) { 45 | // eslint-disable-next-line 46 | console.error(err); 47 | process.exit(1); 48 | } 49 | } 50 | 51 | export async function prepareMetapak($ = new Knifecycle()) { 52 | $.register(initMetapak); 53 | $.register(constant('ENV', env)); 54 | $.register(constant('MAIN_FILE_URL', import.meta.url)); 55 | $.register(constant('exit', exit)); 56 | $.register(constant('EOL', os.EOL)); 57 | $.register(constant('glob', glob)); 58 | $.register( 59 | constant('logger', { 60 | // eslint-disable-next-line 61 | output: console.info.bind(console), 62 | // eslint-disable-next-line 63 | error: console.error.bind(console), 64 | // eslint-disable-next-line 65 | debug: initDebug('metapak'), 66 | }), 67 | ); 68 | $.register(initLog); 69 | $.register(initImporter); 70 | $.register(initResolve); 71 | $.register(name('PROJECT_DIR', initProjectDir)); 72 | $.register(name('GIT_HOOKS_DIR', autoService(initGitHooksDir))); 73 | 74 | $.register(initBuildPackageConf); 75 | $.register(initBuildPackageAssets); 76 | $.register(initBuildPackageGitHooks); 77 | $.register(initProgramOptions); 78 | $.register(initFS); 79 | 80 | return $; 81 | } 82 | 83 | async function initGitHooksDir({ PROJECT_DIR, fs, log }) { 84 | return new Promise((resolve) => { 85 | exec( 86 | 'git rev-parse --git-dir', 87 | { 88 | cwd: PROJECT_DIR, 89 | }, 90 | (err, stdout, stderr) => { 91 | const outputPath = join(stdout.toString().trim(), 'hooks'); 92 | const GIT_HOOKS_DIR = isAbsolute(outputPath) 93 | ? outputPath 94 | : join(PROJECT_DIR, outputPath); 95 | 96 | if (err || !stdout) { 97 | log('debug', '🤷 - Could not find hooks dir.', err ? err.stack : ''); 98 | log('debug', 'stdout:', stdout); 99 | log('debug', 'stderr:', stderr); 100 | resolve(''); 101 | return; 102 | } 103 | log('debug', '✅ - Found hooks dir:', GIT_HOOKS_DIR); 104 | 105 | // Check the dir exists in order to avoid bugs in non-git 106 | // envs (docker images for instance) 107 | fs.accessAsync(GIT_HOOKS_DIR, fs.constants.W_OK) 108 | .then(() => { 109 | log('debug', '✅ - Hooks dir exists:', GIT_HOOKS_DIR); 110 | resolve(GIT_HOOKS_DIR); 111 | }) 112 | .catch((err2) => { 113 | log('debug', '🤷 - Hooks dir does not exist:', GIT_HOOKS_DIR); 114 | log('stack', err2.stack); 115 | resolve(''); 116 | }); 117 | }, 118 | ); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /src/libs/utils.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { diffJson } from 'diff'; 3 | import type { JsonValue, JsonObject, PackageJson } from 'type-fest'; 4 | 5 | export type MetapakConfiguration = { 6 | configs: string[]; 7 | sequence?: string[]; 8 | data: T; 9 | }; 10 | export type MetapakPackageJson = PackageJson & { 11 | metapak: MetapakConfiguration; 12 | } & U; 13 | export type MetapakModuleConfigs = Record< 14 | string, 15 | { 16 | base: string; 17 | srcDir: string; 18 | assetsDir: string; 19 | configs: string[]; 20 | } 21 | >; 22 | export type MetapakContext = { 23 | modulesConfigs: MetapakModuleConfigs; 24 | modulesSequence: string[]; 25 | configsSequence: string[]; 26 | }; 27 | export type PackageJSONTransformer = ( 28 | packageJSON: MetapakPackageJson, 29 | ) => MetapakPackageJson; 30 | 31 | export const identity = (x: T): T => x; 32 | export const identityAsync = async (x: T): Promise => x; 33 | 34 | export async function mapConfigsSequentially( 35 | metapakContext: MetapakContext, 36 | fn: (metapakModuleName: string, metapakModuleConfig: string) => Promise, 37 | ): Promise { 38 | const transformers: T[] = []; 39 | 40 | for (const configName of metapakContext.configsSequence) { 41 | for (const moduleName of metapakContext.modulesSequence) { 42 | const transformer = await fn(moduleName, configName); 43 | transformers.push(transformer); 44 | } 45 | } 46 | 47 | return transformers; 48 | } 49 | 50 | export function buildDiff(newData: JsonValue, originalData: JsonValue): string { 51 | return diffJson(originalData, newData, {}) 52 | .map((part) => 53 | (part.added ? chalk.green : part.removed ? chalk.red : chalk.grey)( 54 | part.value, 55 | ), 56 | ) 57 | .join(''); 58 | } 59 | -------------------------------------------------------------------------------- /src/services/assets.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, test, jest, expect } from '@jest/globals'; 2 | import { Knifecycle, constant } from 'knifecycle'; 3 | import initBuildPackageAssets from './assets.js'; 4 | import type { 5 | BuildPackageAssetsService, 6 | PackageAssetsTransformer, 7 | } from './assets.js'; 8 | import type { ImporterService, LogService } from 'common-services'; 9 | import type { FSService } from './fs.js'; 10 | import type { MetapakPackageJson } from '../libs/utils.js'; 11 | import type { JsonObject } from 'type-fest'; 12 | 13 | describe('buildPackageAssets', () => { 14 | const readFileAsync = jest.fn(); 15 | const writeFileAsync = jest.fn(); 16 | const unlinkAsync = jest.fn(); 17 | const mkdirpAsync = jest.fn(); 18 | const importer = jest.fn< 19 | ImporterService<{ 20 | default: PackageAssetsTransformer; 21 | }> 22 | >(); 23 | const glob = jest.fn<() => Promise>(); 24 | const log = jest.fn(); 25 | let $: Knifecycle; 26 | 27 | beforeEach(() => { 28 | readFileAsync.mockReset(); 29 | writeFileAsync.mockReset(); 30 | unlinkAsync.mockReset(); 31 | mkdirpAsync.mockReset(); 32 | importer.mockReset(); 33 | glob.mockReset(); 34 | log.mockReset(); 35 | 36 | $ = new Knifecycle(); 37 | $.register(constant('ENV', {})); 38 | $.register(constant('log', log)); 39 | $.register(constant('glob', glob)); 40 | $.register(constant('PROJECT_DIR', 'project/dir')); 41 | $.register( 42 | constant('fs', { 43 | readFileAsync: readFileAsync, 44 | writeFileAsync: writeFileAsync, 45 | unlinkAsync: unlinkAsync, 46 | mkdirpAsync: mkdirpAsync, 47 | }), 48 | ); 49 | $.register(constant('importer', importer)); 50 | $.register(initBuildPackageAssets); 51 | }); 52 | 53 | test('should work when data changed', async () => { 54 | const packageConf: MetapakPackageJson = { 55 | metapak: { 56 | configs: ['_common', 'author'], 57 | data: {}, 58 | }, 59 | }; 60 | 61 | importer.mockResolvedValue({ 62 | default: async (file) => { 63 | file.data = '{\n "private": false\n}'; 64 | return file; 65 | }, 66 | }); 67 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 68 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 69 | writeFileAsync.mockResolvedValue(undefined); 70 | unlinkAsync.mockResolvedValue(undefined); 71 | mkdirpAsync.mockResolvedValue(undefined); 72 | glob.mockResolvedValue(['lol']); 73 | 74 | const { buildPackageAssets } = await $.run<{ 75 | buildPackageAssets: BuildPackageAssetsService; 76 | }>(['buildPackageAssets']); 77 | const result = await buildPackageAssets(packageConf, { 78 | configsSequence: ['_common', 'author'], 79 | modulesSequence: ['metapak-http-server'], 80 | modulesConfigs: { 81 | 'metapak-http-server': { 82 | base: 'project/dir/node_modules/metapak-http-server', 83 | srcDir: 'src', 84 | assetsDir: 'src', 85 | configs: ['_common'], 86 | }, 87 | }, 88 | }); 89 | 90 | expect({ 91 | globCalls: glob.mock.calls, 92 | importerCalls: importer.mock.calls, 93 | readFileAsyncCalls: readFileAsync.mock.calls, 94 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 95 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 96 | unlinkAsyncCalls: unlinkAsync.mock.calls, 97 | logCalls: log.mock.calls.filter(filterLogs), 98 | result, 99 | }).toMatchInlineSnapshot(` 100 | { 101 | "globCalls": [ 102 | [ 103 | "**/*", 104 | { 105 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 106 | "dot": true, 107 | "nodir": true, 108 | }, 109 | ], 110 | [ 111 | "**/*", 112 | { 113 | "cwd": "project/dir/node_modules/metapak-http-server/src/author/assets", 114 | "dot": true, 115 | "nodir": true, 116 | }, 117 | ], 118 | ], 119 | "importerCalls": [ 120 | [ 121 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 122 | ], 123 | [ 124 | "project/dir/node_modules/metapak-http-server/src/author/assets.js", 125 | ], 126 | ], 127 | "logCalls": [ 128 | [ 129 | "debug", 130 | "Processing asset:", 131 | "project/dir/node_modules/metapak-http-server/src/author/assets/lol", 132 | ], 133 | [ 134 | "warning", 135 | "💾 - Saving asset:", 136 | "project/dir/lol", 137 | ], 138 | ], 139 | "mkdirpAsyncCalls": [], 140 | "readFileAsyncCalls": [ 141 | [ 142 | "project/dir/node_modules/metapak-http-server/src/author/assets/lol", 143 | ], 144 | [ 145 | "project/dir/lol", 146 | ], 147 | ], 148 | "result": true, 149 | "unlinkAsyncCalls": [], 150 | "writeFileAsyncCalls": [ 151 | [ 152 | "project/dir/lol", 153 | "{ 154 | "private": false 155 | }", 156 | undefined, 157 | ], 158 | ], 159 | } 160 | `); 161 | }); 162 | 163 | test('should rename _dot_ prefixed files', async () => { 164 | const packageConf: MetapakPackageJson = { 165 | metapak: { 166 | configs: ['_common'], 167 | data: {}, 168 | }, 169 | }; 170 | 171 | importer.mockResolvedValue({ 172 | default: async (file) => { 173 | file.data = '{\n "private": false\n}'; 174 | return file; 175 | }, 176 | }); 177 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 178 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 179 | writeFileAsync.mockResolvedValue(undefined); 180 | unlinkAsync.mockResolvedValue(undefined); 181 | mkdirpAsync.mockResolvedValue(undefined); 182 | glob.mockResolvedValue(['_dot_gitignore']); 183 | 184 | const { buildPackageAssets } = await $.run<{ 185 | buildPackageAssets: BuildPackageAssetsService; 186 | }>(['buildPackageAssets']); 187 | const result = await buildPackageAssets(packageConf, { 188 | configsSequence: ['_common'], 189 | modulesSequence: ['metapak-http-server'], 190 | modulesConfigs: { 191 | 'metapak-http-server': { 192 | base: 'project/dir/node_modules/metapak-http-server', 193 | srcDir: 'src', 194 | assetsDir: 'src', 195 | configs: ['_common'], 196 | }, 197 | }, 198 | }); 199 | 200 | expect({ 201 | globCalls: glob.mock.calls, 202 | importerCalls: importer.mock.calls, 203 | readFileAsyncCalls: readFileAsync.mock.calls, 204 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 205 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 206 | unlinkAsyncCalls: unlinkAsync.mock.calls, 207 | logCalls: log.mock.calls.filter(filterLogs), 208 | result, 209 | }).toMatchInlineSnapshot(` 210 | { 211 | "globCalls": [ 212 | [ 213 | "**/*", 214 | { 215 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 216 | "dot": true, 217 | "nodir": true, 218 | }, 219 | ], 220 | ], 221 | "importerCalls": [ 222 | [ 223 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 224 | ], 225 | ], 226 | "logCalls": [ 227 | [ 228 | "debug", 229 | "Processing asset:", 230 | "project/dir/node_modules/metapak-http-server/src/_common/assets/_dot_gitignore", 231 | ], 232 | [ 233 | "warning", 234 | "💾 - Saving asset:", 235 | "project/dir/.gitignore", 236 | ], 237 | ], 238 | "mkdirpAsyncCalls": [], 239 | "readFileAsyncCalls": [ 240 | [ 241 | "project/dir/node_modules/metapak-http-server/src/_common/assets/_dot_gitignore", 242 | ], 243 | [ 244 | "project/dir/.gitignore", 245 | ], 246 | ], 247 | "result": true, 248 | "unlinkAsyncCalls": [], 249 | "writeFileAsyncCalls": [ 250 | [ 251 | "project/dir/.gitignore", 252 | "{ 253 | "private": false 254 | }", 255 | undefined, 256 | ], 257 | ], 258 | } 259 | `); 260 | }); 261 | 262 | test('should warn on using .gitignore files', async () => { 263 | const packageConf: MetapakPackageJson = { 264 | metapak: { 265 | configs: ['_common'], 266 | data: {}, 267 | }, 268 | }; 269 | 270 | importer.mockResolvedValue({ 271 | default: async (file) => { 272 | file.data = '{\n "private": false\n}'; 273 | return file; 274 | }, 275 | }); 276 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 277 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 278 | writeFileAsync.mockResolvedValue(undefined); 279 | unlinkAsync.mockResolvedValue(undefined); 280 | mkdirpAsync.mockResolvedValue(undefined); 281 | glob.mockResolvedValue(['.gitignore']); 282 | 283 | const { buildPackageAssets } = await $.run<{ 284 | buildPackageAssets: BuildPackageAssetsService; 285 | }>(['buildPackageAssets']); 286 | const result = await buildPackageAssets(packageConf, { 287 | configsSequence: ['_common'], 288 | modulesSequence: ['metapak-http-server'], 289 | modulesConfigs: { 290 | 'metapak-http-server': { 291 | base: 'project/dir/node_modules/metapak-http-server', 292 | srcDir: 'src', 293 | assetsDir: 'src', 294 | configs: ['_common'], 295 | }, 296 | }, 297 | }); 298 | 299 | expect({ 300 | globCalls: glob.mock.calls, 301 | importerCalls: importer.mock.calls, 302 | readFileAsyncCalls: readFileAsync.mock.calls, 303 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 304 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 305 | unlinkAsyncCalls: unlinkAsync.mock.calls, 306 | logCalls: log.mock.calls.filter(filterLogs), 307 | result, 308 | }).toMatchInlineSnapshot(` 309 | { 310 | "globCalls": [ 311 | [ 312 | "**/*", 313 | { 314 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 315 | "dot": true, 316 | "nodir": true, 317 | }, 318 | ], 319 | ], 320 | "importerCalls": [ 321 | [ 322 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 323 | ], 324 | ], 325 | "logCalls": [ 326 | [ 327 | "warning", 328 | "⚠️ - \`.gitignore\` assets may not work, use \`_dot_\` instead of a raw \`.\` in your \`assets\` folder, metapak will care to rename them correctly. See https://github.com/npm/npm/issues/15660", 329 | ], 330 | [ 331 | "debug", 332 | "Processing asset:", 333 | "project/dir/node_modules/metapak-http-server/src/_common/assets/.gitignore", 334 | ], 335 | [ 336 | "warning", 337 | "💾 - Saving asset:", 338 | "project/dir/.gitignore", 339 | ], 340 | ], 341 | "mkdirpAsyncCalls": [], 342 | "readFileAsyncCalls": [ 343 | [ 344 | "project/dir/node_modules/metapak-http-server/src/_common/assets/.gitignore", 345 | ], 346 | [ 347 | "project/dir/.gitignore", 348 | ], 349 | ], 350 | "result": true, 351 | "unlinkAsyncCalls": [], 352 | "writeFileAsyncCalls": [ 353 | [ 354 | "project/dir/.gitignore", 355 | "{ 356 | "private": false 357 | }", 358 | undefined, 359 | ], 360 | ], 361 | } 362 | `); 363 | }); 364 | 365 | test('should work whith several transformers', async () => { 366 | const packageConf: MetapakPackageJson = { 367 | metapak: { 368 | configs: ['_common'], 369 | data: {}, 370 | }, 371 | }; 372 | 373 | importer.mockResolvedValueOnce({ 374 | default: async (file) => { 375 | file.data += 'node_modules\n'; 376 | return file; 377 | }, 378 | }); 379 | importer.mockResolvedValueOnce({ 380 | default: async (file) => { 381 | file.data += 'coverage\n'; 382 | return file; 383 | }, 384 | }); 385 | readFileAsync.mockResolvedValueOnce(Buffer.from('.git\n')); 386 | readFileAsync.mockResolvedValueOnce(Buffer.from('.git\n.lol\n')); 387 | writeFileAsync.mockResolvedValue(undefined); 388 | unlinkAsync.mockResolvedValue(undefined); 389 | glob.mockResolvedValueOnce(['_dot_gitignore']); 390 | mkdirpAsync.mockResolvedValue(undefined); 391 | glob.mockResolvedValueOnce([]); 392 | 393 | const { buildPackageAssets } = await $.run<{ 394 | buildPackageAssets: BuildPackageAssetsService; 395 | }>(['buildPackageAssets']); 396 | const result = await buildPackageAssets(packageConf, { 397 | configsSequence: ['_common'], 398 | modulesSequence: ['metapak-module1', 'metapak-module2'], 399 | modulesConfigs: { 400 | 'metapak-module1': { 401 | base: 'project/dir/node_modules/metapak-module1', 402 | srcDir: 'src', 403 | assetsDir: 'src', 404 | configs: ['_common'], 405 | }, 406 | 'metapak-module2': { 407 | base: 'project/dir/node_modules/metapak-module2', 408 | srcDir: 'src', 409 | assetsDir: 'src', 410 | configs: ['_common'], 411 | }, 412 | }, 413 | }); 414 | 415 | expect({ 416 | globCalls: glob.mock.calls, 417 | importerCalls: importer.mock.calls, 418 | readFileAsyncCalls: readFileAsync.mock.calls, 419 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 420 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 421 | unlinkAsyncCalls: unlinkAsync.mock.calls, 422 | logCalls: log.mock.calls.filter(filterLogs), 423 | result, 424 | }).toMatchInlineSnapshot(` 425 | { 426 | "globCalls": [ 427 | [ 428 | "**/*", 429 | { 430 | "cwd": "project/dir/node_modules/metapak-module1/src/_common/assets", 431 | "dot": true, 432 | "nodir": true, 433 | }, 434 | ], 435 | [ 436 | "**/*", 437 | { 438 | "cwd": "project/dir/node_modules/metapak-module2/src/_common/assets", 439 | "dot": true, 440 | "nodir": true, 441 | }, 442 | ], 443 | ], 444 | "importerCalls": [ 445 | [ 446 | "project/dir/node_modules/metapak-module1/src/_common/assets.js", 447 | ], 448 | [ 449 | "project/dir/node_modules/metapak-module2/src/_common/assets.js", 450 | ], 451 | ], 452 | "logCalls": [ 453 | [ 454 | "debug", 455 | "Processing asset:", 456 | "project/dir/node_modules/metapak-module1/src/_common/assets/_dot_gitignore", 457 | ], 458 | [ 459 | "warning", 460 | "💾 - Saving asset:", 461 | "project/dir/.gitignore", 462 | ], 463 | ], 464 | "mkdirpAsyncCalls": [], 465 | "readFileAsyncCalls": [ 466 | [ 467 | "project/dir/node_modules/metapak-module1/src/_common/assets/_dot_gitignore", 468 | ], 469 | [ 470 | "project/dir/.gitignore", 471 | ], 472 | ], 473 | "result": true, 474 | "unlinkAsyncCalls": [], 475 | "writeFileAsyncCalls": [ 476 | [ 477 | "project/dir/.gitignore", 478 | ".git 479 | node_modules 480 | coverage 481 | ", 482 | undefined, 483 | ], 484 | ], 485 | } 486 | `); 487 | }); 488 | 489 | test('should work whith directories', async () => { 490 | const packageConf: MetapakPackageJson = { 491 | metapak: { 492 | configs: ['_common'], 493 | data: {}, 494 | }, 495 | }; 496 | 497 | importer.mockResolvedValue({ 498 | default: async (file) => { 499 | file.data = '{\n "private": false\n}'; 500 | return file; 501 | }, 502 | }); 503 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 504 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 505 | writeFileAsync.mockResolvedValue(undefined); 506 | unlinkAsync.mockResolvedValue(undefined); 507 | mkdirpAsync.mockResolvedValue(undefined); 508 | glob.mockResolvedValue(['lol/wadup']); 509 | 510 | const { buildPackageAssets } = await $.run<{ 511 | buildPackageAssets: BuildPackageAssetsService; 512 | }>(['buildPackageAssets']); 513 | const result = await buildPackageAssets(packageConf, { 514 | configsSequence: ['_common'], 515 | modulesSequence: ['metapak-http-server'], 516 | modulesConfigs: { 517 | 'metapak-http-server': { 518 | base: 'project/dir/node_modules/metapak-http-server', 519 | srcDir: 'src', 520 | assetsDir: 'src', 521 | configs: ['_common'], 522 | }, 523 | }, 524 | }); 525 | 526 | expect({ 527 | globCalls: glob.mock.calls, 528 | importerCalls: importer.mock.calls, 529 | readFileAsyncCalls: readFileAsync.mock.calls, 530 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 531 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 532 | unlinkAsyncCalls: unlinkAsync.mock.calls, 533 | logCalls: log.mock.calls.filter(filterLogs), 534 | result, 535 | }).toMatchInlineSnapshot(` 536 | { 537 | "globCalls": [ 538 | [ 539 | "**/*", 540 | { 541 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 542 | "dot": true, 543 | "nodir": true, 544 | }, 545 | ], 546 | ], 547 | "importerCalls": [ 548 | [ 549 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 550 | ], 551 | ], 552 | "logCalls": [ 553 | [ 554 | "debug", 555 | "Processing asset:", 556 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol/wadup", 557 | ], 558 | [ 559 | "warning", 560 | "💾 - Saving asset:", 561 | "project/dir/lol/wadup", 562 | ], 563 | [ 564 | "warning", 565 | "📁 - Creating a directory:", 566 | "lol", 567 | ], 568 | ], 569 | "mkdirpAsyncCalls": [ 570 | [ 571 | "project/dir/lol", 572 | ], 573 | ], 574 | "readFileAsyncCalls": [ 575 | [ 576 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol/wadup", 577 | ], 578 | [ 579 | "project/dir/lol/wadup", 580 | ], 581 | ], 582 | "result": true, 583 | "unlinkAsyncCalls": [], 584 | "writeFileAsyncCalls": [ 585 | [ 586 | "project/dir/lol/wadup", 587 | "{ 588 | "private": false 589 | }", 590 | undefined, 591 | ], 592 | ], 593 | } 594 | `); 595 | }); 596 | 597 | test('should allow to rename assets with async transformers', async () => { 598 | const packageConf: MetapakPackageJson = { 599 | metapak: { 600 | configs: ['_common'], 601 | data: {}, 602 | }, 603 | }; 604 | 605 | importer.mockResolvedValue({ 606 | default: async (file) => { 607 | file.name = 'notlol'; 608 | file.data = '{\n "private": false\n}'; 609 | return Promise.resolve(file); 610 | }, 611 | }); 612 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 613 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 614 | writeFileAsync.mockResolvedValue(undefined); 615 | unlinkAsync.mockResolvedValue(undefined); 616 | mkdirpAsync.mockResolvedValue(undefined); 617 | glob.mockResolvedValue(['lol']); 618 | 619 | const { buildPackageAssets } = await $.run<{ 620 | buildPackageAssets: BuildPackageAssetsService; 621 | }>(['buildPackageAssets']); 622 | const result = await buildPackageAssets(packageConf, { 623 | configsSequence: ['_common'], 624 | modulesSequence: ['metapak-http-server'], 625 | modulesConfigs: { 626 | 'metapak-http-server': { 627 | base: 'project/dir/node_modules/metapak-http-server', 628 | srcDir: 'src', 629 | assetsDir: 'src', 630 | configs: ['_common'], 631 | }, 632 | }, 633 | }); 634 | 635 | expect({ 636 | globCalls: glob.mock.calls, 637 | importerCalls: importer.mock.calls, 638 | readFileAsyncCalls: readFileAsync.mock.calls, 639 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 640 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 641 | unlinkAsyncCalls: unlinkAsync.mock.calls, 642 | logCalls: log.mock.calls.filter(filterLogs), 643 | result, 644 | }).toMatchInlineSnapshot(` 645 | { 646 | "globCalls": [ 647 | [ 648 | "**/*", 649 | { 650 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 651 | "dot": true, 652 | "nodir": true, 653 | }, 654 | ], 655 | ], 656 | "importerCalls": [ 657 | [ 658 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 659 | ], 660 | ], 661 | "logCalls": [ 662 | [ 663 | "debug", 664 | "Processing asset:", 665 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 666 | ], 667 | [ 668 | "warning", 669 | "💾 - Saving asset:", 670 | "project/dir/notlol", 671 | ], 672 | ], 673 | "mkdirpAsyncCalls": [], 674 | "readFileAsyncCalls": [ 675 | [ 676 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 677 | ], 678 | [ 679 | "project/dir/notlol", 680 | ], 681 | ], 682 | "result": true, 683 | "unlinkAsyncCalls": [], 684 | "writeFileAsyncCalls": [ 685 | [ 686 | "project/dir/notlol", 687 | "{ 688 | "private": false 689 | }", 690 | undefined, 691 | ], 692 | ], 693 | } 694 | `); 695 | }); 696 | 697 | test('should work when data did not change', async () => { 698 | const packageConf: MetapakPackageJson = { 699 | metapak: { 700 | configs: ['_common'], 701 | data: {}, 702 | }, 703 | }; 704 | 705 | importer.mockResolvedValue({ 706 | default: async (file) => { 707 | file.data = '{\n "private": true\n}'; 708 | return file; 709 | }, 710 | }); 711 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 712 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 713 | writeFileAsync.mockResolvedValue(undefined); 714 | unlinkAsync.mockResolvedValue(undefined); 715 | mkdirpAsync.mockResolvedValue(undefined); 716 | glob.mockResolvedValue(['lol']); 717 | 718 | const { buildPackageAssets } = await $.run<{ 719 | buildPackageAssets: BuildPackageAssetsService; 720 | }>(['buildPackageAssets']); 721 | const result = await buildPackageAssets(packageConf, { 722 | configsSequence: ['_common'], 723 | modulesSequence: ['metapak-http-server'], 724 | modulesConfigs: { 725 | 'metapak-http-server': { 726 | base: 'project/dir/node_modules/metapak-http-server', 727 | srcDir: 'src', 728 | assetsDir: 'src', 729 | configs: ['_common'], 730 | }, 731 | }, 732 | }); 733 | 734 | expect({ 735 | globCalls: glob.mock.calls, 736 | importerCalls: importer.mock.calls, 737 | readFileAsyncCalls: readFileAsync.mock.calls, 738 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 739 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 740 | unlinkAsyncCalls: unlinkAsync.mock.calls, 741 | logCalls: log.mock.calls.filter(filterLogs), 742 | result, 743 | }).toMatchInlineSnapshot(` 744 | { 745 | "globCalls": [ 746 | [ 747 | "**/*", 748 | { 749 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 750 | "dot": true, 751 | "nodir": true, 752 | }, 753 | ], 754 | ], 755 | "importerCalls": [ 756 | [ 757 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 758 | ], 759 | ], 760 | "logCalls": [ 761 | [ 762 | "debug", 763 | "Processing asset:", 764 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 765 | ], 766 | ], 767 | "mkdirpAsyncCalls": [], 768 | "readFileAsyncCalls": [ 769 | [ 770 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 771 | ], 772 | [ 773 | "project/dir/lol", 774 | ], 775 | ], 776 | "result": false, 777 | "unlinkAsyncCalls": [], 778 | "writeFileAsyncCalls": [], 779 | } 780 | `); 781 | }); 782 | 783 | test('should delete when data is empty', async () => { 784 | const packageConf: MetapakPackageJson = { 785 | metapak: { 786 | configs: ['_common'], 787 | data: {}, 788 | }, 789 | }; 790 | 791 | importer.mockResolvedValue({ 792 | default: async (file) => { 793 | file.data = ''; 794 | return file; 795 | }, 796 | }); 797 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 798 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "private": true\n}')); 799 | writeFileAsync.mockResolvedValue(undefined); 800 | unlinkAsync.mockResolvedValue(undefined); 801 | mkdirpAsync.mockResolvedValue(undefined); 802 | glob.mockResolvedValue(['lol']); 803 | 804 | const { buildPackageAssets } = await $.run<{ 805 | buildPackageAssets: BuildPackageAssetsService; 806 | }>(['buildPackageAssets']); 807 | const result = await buildPackageAssets(packageConf, { 808 | configsSequence: ['_common'], 809 | modulesSequence: ['metapak-http-server'], 810 | modulesConfigs: { 811 | 'metapak-http-server': { 812 | base: 'project/dir/node_modules/metapak-http-server', 813 | srcDir: 'src', 814 | assetsDir: 'src', 815 | configs: ['_common'], 816 | }, 817 | }, 818 | }); 819 | 820 | expect({ 821 | globCalls: glob.mock.calls, 822 | importerCalls: importer.mock.calls, 823 | readFileAsyncCalls: readFileAsync.mock.calls, 824 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 825 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 826 | unlinkAsyncCalls: unlinkAsync.mock.calls, 827 | logCalls: log.mock.calls.filter(filterLogs), 828 | result, 829 | }).toMatchInlineSnapshot(` 830 | { 831 | "globCalls": [ 832 | [ 833 | "**/*", 834 | { 835 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 836 | "dot": true, 837 | "nodir": true, 838 | }, 839 | ], 840 | ], 841 | "importerCalls": [ 842 | [ 843 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 844 | ], 845 | ], 846 | "logCalls": [ 847 | [ 848 | "debug", 849 | "Processing asset:", 850 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 851 | ], 852 | [ 853 | "warning", 854 | "⌫ - Deleting asset:", 855 | "project/dir/lol", 856 | ], 857 | ], 858 | "mkdirpAsyncCalls": [], 859 | "readFileAsyncCalls": [ 860 | [ 861 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 862 | ], 863 | [ 864 | "project/dir/lol", 865 | ], 866 | ], 867 | "result": true, 868 | "unlinkAsyncCalls": [ 869 | [ 870 | "project/dir/lol", 871 | ], 872 | ], 873 | "writeFileAsyncCalls": [], 874 | } 875 | `); 876 | }); 877 | 878 | test('should not delete when data is empty and file is already deleted', async () => { 879 | const packageConf: MetapakPackageJson = { 880 | metapak: { 881 | configs: ['_common'], 882 | data: {}, 883 | }, 884 | }; 885 | 886 | importer.mockResolvedValue({ 887 | default: async (file) => { 888 | file.data = ''; 889 | return file; 890 | }, 891 | }); 892 | readFileAsync.mockResolvedValueOnce(Buffer.from('{\n "test": true\n}')); 893 | readFileAsync.mockRejectedValueOnce(new Error('E_NOT_FOUND')); 894 | writeFileAsync.mockResolvedValue(undefined); 895 | unlinkAsync.mockResolvedValue(undefined); 896 | mkdirpAsync.mockResolvedValue(undefined); 897 | glob.mockResolvedValue(['lol']); 898 | 899 | const { buildPackageAssets } = await $.run<{ 900 | buildPackageAssets: BuildPackageAssetsService; 901 | }>(['buildPackageAssets']); 902 | const result = await buildPackageAssets(packageConf, { 903 | configsSequence: ['_common'], 904 | modulesSequence: ['metapak-http-server'], 905 | modulesConfigs: { 906 | 'metapak-http-server': { 907 | base: 'project/dir/node_modules/metapak-http-server', 908 | srcDir: 'src', 909 | assetsDir: 'src', 910 | configs: ['_common'], 911 | }, 912 | }, 913 | }); 914 | 915 | expect({ 916 | globCalls: glob.mock.calls, 917 | importerCalls: importer.mock.calls, 918 | readFileAsyncCalls: readFileAsync.mock.calls, 919 | mkdirpAsyncCalls: mkdirpAsync.mock.calls, 920 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 921 | unlinkAsyncCalls: unlinkAsync.mock.calls, 922 | logCalls: log.mock.calls.filter(filterLogs), 923 | result, 924 | }).toMatchInlineSnapshot(` 925 | { 926 | "globCalls": [ 927 | [ 928 | "**/*", 929 | { 930 | "cwd": "project/dir/node_modules/metapak-http-server/src/_common/assets", 931 | "dot": true, 932 | "nodir": true, 933 | }, 934 | ], 935 | ], 936 | "importerCalls": [ 937 | [ 938 | "project/dir/node_modules/metapak-http-server/src/_common/assets.js", 939 | ], 940 | ], 941 | "logCalls": [ 942 | [ 943 | "debug", 944 | "Processing asset:", 945 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 946 | ], 947 | [ 948 | "debug", 949 | "🤷 - Asset not found:", 950 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 951 | ], 952 | ], 953 | "mkdirpAsyncCalls": [], 954 | "readFileAsyncCalls": [ 955 | [ 956 | "project/dir/node_modules/metapak-http-server/src/_common/assets/lol", 957 | ], 958 | [ 959 | "project/dir/lol", 960 | ], 961 | ], 962 | "result": false, 963 | "unlinkAsyncCalls": [], 964 | "writeFileAsyncCalls": [], 965 | } 966 | `); 967 | }); 968 | }); 969 | 970 | function bufferToText( 971 | call: Parameters, 972 | ): [string, string, Parameters[2]] { 973 | return [call[0], call[1].toString(), call[2]]; 974 | } 975 | 976 | function filterLogs(e: Parameters) { 977 | return !e[0].endsWith('-stack'); 978 | } 979 | -------------------------------------------------------------------------------- /src/services/assets.ts: -------------------------------------------------------------------------------- 1 | import { autoService } from 'knifecycle'; 2 | import path from 'path'; 3 | import { identityAsync, mapConfigsSequentially } from '../libs/utils.js'; 4 | import { YError, printStackTrace } from 'yerror'; 5 | import type { MetapakContext, MetapakPackageJson } from '../libs/utils.js'; 6 | import type { GlobOptions } from 'glob'; 7 | import type { FSService } from './fs.js'; 8 | import type { ImporterService, LogService } from 'common-services'; 9 | 10 | export type BuildPackageAssetsService = ( 11 | packageConf: MetapakPackageJson, 12 | metapakContext: MetapakContext, 13 | ) => Promise; 14 | export type AssetFile = { 15 | dir: string; 16 | name: string; 17 | data: string; 18 | }; 19 | export type PackageAssetsTransformer = ( 20 | file: AssetFile, 21 | packageConf: MetapakPackageJson, 22 | services: { 23 | PROJECT_DIR: string; 24 | log: LogService; 25 | fs: FSService; 26 | }, 27 | ) => Promise; 28 | 29 | export default autoService(initBuildPackageAssets); 30 | 31 | async function initBuildPackageAssets({ 32 | PROJECT_DIR, 33 | fs, 34 | log, 35 | glob, 36 | importer, 37 | }: { 38 | PROJECT_DIR: string; 39 | fs: FSService; 40 | log: LogService; 41 | glob: (pattern: string, options: GlobOptions) => Promise; 42 | importer: ImporterService<{ 43 | default: PackageAssetsTransformer; 44 | }>; 45 | }) { 46 | return async ( 47 | packageConf: MetapakPackageJson, 48 | metapakContext: MetapakContext, 49 | ) => { 50 | const assetsDirsGroups = await mapConfigsSequentially<{ 51 | assets: AssetFile[]; 52 | transformer: PackageAssetsTransformer; 53 | }>(metapakContext, async (metapakModuleName, metapakConfigName) => { 54 | const packageAssetsDir = path.join( 55 | metapakContext.modulesConfigs[metapakModuleName].base, 56 | metapakContext.modulesConfigs[metapakModuleName].assetsDir, 57 | metapakConfigName, 58 | 'assets', 59 | ); 60 | const packageAssetsTransformerPath = path.join( 61 | metapakContext.modulesConfigs[metapakModuleName].base, 62 | metapakContext.modulesConfigs[metapakModuleName].srcDir, 63 | metapakConfigName, 64 | 'assets.js', 65 | ); 66 | let transformer: PackageAssetsTransformer; 67 | 68 | try { 69 | transformer = (await importer(packageAssetsTransformerPath)).default; 70 | } catch (err) { 71 | log( 72 | 'debug', 73 | '🤷 - No asset tranformation found at:', 74 | packageAssetsTransformerPath, 75 | ); 76 | log('debug-stack', printStackTrace(err as YError)); 77 | transformer = identityAsync; 78 | } 79 | 80 | try { 81 | const assetsNames = await glob('**/*', { 82 | cwd: packageAssetsDir, 83 | dot: true, 84 | nodir: true, 85 | }); 86 | 87 | if (assetsNames.some((asset) => '.gitignore' === asset)) { 88 | log( 89 | 'warning', 90 | '⚠️ - `.gitignore` assets may not work, use `_dot_` instead of a raw `.` in your `assets` folder, metapak will care to rename them correctly. See https://github.com/npm/npm/issues/15660', 91 | ); 92 | } 93 | const assets: AssetFile[] = assetsNames.map((asset) => ({ 94 | dir: packageAssetsDir, 95 | name: asset, 96 | data: '', 97 | })); 98 | return { assets, transformer }; 99 | } catch (err) { 100 | log('debug', '🤷 - No assets found at:', packageAssetsDir); 101 | log('debug-stack', printStackTrace(err as YError)); 102 | return { assets: [], transformer }; 103 | } 104 | }); 105 | 106 | const { assets, transformers } = await assetsDirsGroups.reduce( 107 | (combined, { assets, transformer }) => ({ 108 | assets: combined.assets.concat(assets), 109 | transformers: combined.transformers.concat(transformer), 110 | }), 111 | { assets: [], transformers: [] } as { 112 | assets: AssetFile[]; 113 | transformers: PackageAssetsTransformer[]; 114 | }, 115 | ); 116 | 117 | // Building the hash dedupes assets by picking them in the upper config 118 | const assetsHash = assets.reduce((hash, { dir, name }) => { 119 | hash[name] = { dir, name }; 120 | return hash; 121 | }, {}); 122 | 123 | const results = await Promise.all( 124 | Object.keys(assetsHash).map( 125 | _processAsset.bind( 126 | null, 127 | { 128 | PROJECT_DIR, 129 | log, 130 | fs, 131 | }, 132 | { 133 | packageConf, 134 | transformers, 135 | assetsHash, 136 | }, 137 | ), 138 | ), 139 | ); 140 | 141 | return results.reduce( 142 | (assetsChanged, assetChanged) => assetsChanged || assetChanged, 143 | false, 144 | ); 145 | }; 146 | } 147 | 148 | async function _processAsset( 149 | { 150 | PROJECT_DIR, 151 | log, 152 | fs, 153 | }: { 154 | PROJECT_DIR: string; 155 | log: LogService; 156 | fs: FSService; 157 | }, 158 | { 159 | packageConf, 160 | transformers, 161 | assetsHash, 162 | }: { 163 | packageConf: MetapakPackageJson; 164 | transformers: PackageAssetsTransformer[]; 165 | assetsHash: Record; 166 | }, 167 | name: string, 168 | ) { 169 | const { dir } = assetsHash[name]; 170 | const assetPath = path.join(dir, name); 171 | 172 | log('debug', 'Processing asset:', assetPath); 173 | 174 | const sourceFile: AssetFile = { 175 | name: name.startsWith('_dot_') ? name.replace('_dot_', '.') : name, 176 | dir, 177 | data: (await fs.readFileAsync(assetPath)).toString(), 178 | }; 179 | let newFile = sourceFile; 180 | 181 | for (const transformer of transformers) { 182 | newFile = await transformer(newFile, packageConf, { 183 | PROJECT_DIR, 184 | fs, 185 | log, 186 | }); 187 | } 188 | 189 | const originalFile: AssetFile = { 190 | name: sourceFile.name, 191 | dir, 192 | data: ( 193 | (await fs 194 | .readFileAsync(path.join(PROJECT_DIR, newFile.name)) 195 | .catch((err) => { 196 | log('debug', '🤷 - Asset not found:', path.join(dir, newFile.name)); 197 | log('debug-stack', printStackTrace(err as YError)); 198 | return Buffer.from(''); 199 | })) as Buffer 200 | ).toString(), 201 | }; 202 | 203 | if (newFile.data === originalFile.data) { 204 | return false; 205 | } 206 | 207 | if ('' === newFile.data) { 208 | if (originalFile.data) { 209 | log( 210 | 'warning', 211 | '⌫ - Deleting asset:', 212 | path.join(PROJECT_DIR, newFile.name), 213 | ); 214 | await fs.unlinkAsync(path.join(PROJECT_DIR, newFile.name)); 215 | 216 | return true; 217 | } 218 | return false; 219 | } 220 | 221 | log('warning', '💾 - Saving asset:', path.join(PROJECT_DIR, newFile.name)); 222 | await _ensureDirExists({ PROJECT_DIR, fs, log }, newFile); 223 | await fs.writeFileAsync( 224 | path.join(PROJECT_DIR, newFile.name), 225 | Buffer.from(newFile.data || ''), 226 | ); 227 | 228 | return true; 229 | } 230 | 231 | async function _ensureDirExists( 232 | { 233 | PROJECT_DIR, 234 | fs, 235 | log, 236 | }: { PROJECT_DIR: string; fs: FSService; log: LogService }, 237 | newFile: AssetFile, 238 | ) { 239 | const dir = path.dirname(newFile.name); 240 | 241 | if ('.' === dir) { 242 | return; 243 | } 244 | 245 | try { 246 | await fs.accessAsync(dir); 247 | } catch (err) { 248 | log('debug-stack', printStackTrace(err as YError)); 249 | log('warning', `📁 - Creating a directory:`, dir); 250 | await fs.mkdirpAsync(path.join(PROJECT_DIR, dir)); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/services/fs.ts: -------------------------------------------------------------------------------- 1 | import { name, autoService } from 'knifecycle'; 2 | import { YError } from 'yerror'; 3 | import fs from 'fs'; 4 | import { mkdirp } from 'mkdirp'; 5 | import type { WriteFileOptions } from 'fs'; 6 | import type { LogService } from 'common-services'; 7 | import type { ProgramOptionsService } from './programOptions.js'; 8 | 9 | export type FSService = { 10 | mkdirpAsync: (path: string) => Promise; 11 | readFileAsync: (path: string) => Promise; 12 | accessAsync: (path: string) => Promise; 13 | readdirAsync: (path: string) => Promise; 14 | unlinkAsync: (path: string) => Promise; 15 | writeFileAsync: ( 16 | path: string, 17 | data: Buffer, 18 | options?: WriteFileOptions, 19 | ) => Promise; 20 | constants: typeof fs.constants; 21 | }; 22 | 23 | async function initFS({ 24 | programOptions, 25 | log, 26 | }: { 27 | programOptions: ProgramOptionsService; 28 | log: LogService; 29 | }): Promise { 30 | return { 31 | mkdirpAsync: async (path: string) => { 32 | if (programOptions.dryRun) { 33 | log('warning', '📂 - Create a folder:', path); 34 | return; 35 | } 36 | await mkdirp(path, { 37 | fs: { 38 | mkdir: (( 39 | ...args: [ 40 | path: string, 41 | callback: ( 42 | err: NodeJS.ErrnoException | null, 43 | path?: string, 44 | ) => void, 45 | ] 46 | ) => { 47 | if (programOptions.safe) { 48 | throw new YError('E_UNEXPECTED_CHANGES', args[0]); 49 | } 50 | fs.mkdir(...args); 51 | }) as typeof fs.mkdir, 52 | stat: fs.stat, 53 | }, 54 | }); 55 | }, 56 | readFileAsync: fs.promises.readFile, 57 | accessAsync: fs.promises.access, 58 | readdirAsync: fs.promises.readdir, 59 | unlinkAsync: async (path) => { 60 | if (programOptions.dryRun) { 61 | log('warning', '⌫ - Delete a file:', path); 62 | return; 63 | } 64 | if (programOptions.safe) { 65 | throw new YError('E_UNEXPECTED_CHANGES', path); 66 | } 67 | await fs.promises.unlink(path); 68 | }, 69 | writeFileAsync: async (path: string, data: Buffer) => { 70 | if (programOptions.dryRun) { 71 | log('warning', '💾 - Modify a file:', path); 72 | return; 73 | } 74 | if (programOptions.safe) { 75 | throw new YError('E_UNEXPECTED_CHANGES', path); 76 | } 77 | await fs.promises.writeFile(path, data); 78 | }, 79 | constants: fs.constants, 80 | }; 81 | } 82 | 83 | export default name('fs', autoService(initFS)); 84 | -------------------------------------------------------------------------------- /src/services/gitHooks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, test, jest, expect } from '@jest/globals'; 2 | import { Knifecycle, constant } from 'knifecycle'; 3 | import initBuildPackageGitHooks from './gitHooks.js'; 4 | import type { 5 | GitHooksTransformer, 6 | BuildPackageGitHooksService, 7 | } from './gitHooks.js'; 8 | import type { ImporterService, LogService } from 'common-services'; 9 | import type { FSService } from './fs.js'; 10 | 11 | describe('buildPackageGitHooks', () => { 12 | const writeFileAsync = jest.fn(); 13 | const readFileAsync = jest.fn(); 14 | const log = jest.fn(); 15 | const importer = 16 | jest.fn< 17 | ImporterService<{ default: GitHooksTransformer }> 18 | >(); 19 | let $: Knifecycle; 20 | 21 | beforeEach(() => { 22 | writeFileAsync.mockReset(); 23 | readFileAsync.mockReset(); 24 | log.mockReset(); 25 | importer.mockReset(); 26 | 27 | $ = new Knifecycle(); 28 | $.register(constant('log', log)); 29 | $.register(constant('PROJECT_DIR', '/home/whoiam/project/dir')); 30 | $.register(constant('EOL', '\n')); 31 | $.register( 32 | constant('fs', { 33 | readFileAsync: readFileAsync, 34 | writeFileAsync: writeFileAsync, 35 | }), 36 | ); 37 | $.register(constant('importer', importer)); 38 | $.register(initBuildPackageGitHooks); 39 | }); 40 | 41 | test('should work with one module and one config', async () => { 42 | $.register(constant('ENV', {})); 43 | $.register( 44 | constant('GIT_HOOKS_DIR', '/home/whoiam/project/dir/.git/hooks'), 45 | ); 46 | 47 | importer.mockResolvedValueOnce({ 48 | default: (hooks) => { 49 | hooks['pre-commit'] = hooks['pre-commit'] || []; 50 | hooks['pre-commit'].push('npm run test && npm run lint || exit 1'); 51 | return hooks; 52 | }, 53 | }); 54 | importer.mockRejectedValueOnce(new Error('E_ERROR_1')); 55 | importer.mockResolvedValueOnce({ 56 | default: (hooks) => { 57 | hooks['pre-commit'] = hooks['pre-commit'] || []; 58 | hooks['pre-commit'].push('npm run coveralls'); 59 | return hooks; 60 | }, 61 | }); 62 | importer.mockRejectedValueOnce(new Error('E_ERROR_2')); 63 | readFileAsync.mockResolvedValueOnce(Buffer.from('')); 64 | writeFileAsync.mockResolvedValueOnce(); 65 | 66 | const { buildPackageGitHooks } = await $.run<{ 67 | buildPackageGitHooks: BuildPackageGitHooksService; 68 | }>(['buildPackageGitHooks']); 69 | 70 | await buildPackageGitHooks( 71 | { 72 | metapak: { 73 | configs: ['_common'], 74 | data: {}, 75 | }, 76 | }, 77 | { 78 | configsSequence: ['_common'], 79 | modulesSequence: ['metapak-nfroidure', 'metapak-fantasia'], 80 | modulesConfigs: { 81 | 'metapak-nfroidure': { 82 | base: '/home/whoiam/project/dir/node_modules/metapak-nfroidure', 83 | srcDir: 'src', 84 | assetsDir: 'src', 85 | configs: ['_common', 'lol'], 86 | }, 87 | 'metapak-fantasia': { 88 | base: '/home/whoiam/project/dir/node_modules/metapak-fantasia', 89 | srcDir: 'src', 90 | assetsDir: 'src', 91 | configs: ['_common', 'test'], 92 | }, 93 | }, 94 | }, 95 | ); 96 | expect(importer.mock.calls).toEqual([ 97 | [ 98 | '/home/whoiam/project/dir/node_modules/metapak-nfroidure/src/_common/hooks.js', 99 | ], 100 | [ 101 | '/home/whoiam/project/dir/node_modules/metapak-fantasia/src/_common/hooks.js', 102 | ], 103 | ]); 104 | expect(writeFileAsync.mock.calls.map(bufferToText)).toEqual([ 105 | [ 106 | '/home/whoiam/project/dir/.git/hooks/pre-commit', 107 | '#!/bin/sh\n' + 108 | '# Automagically generated by metapak, do not change in place.\n' + 109 | '# Your changes would be loose on the next npm install run.\n' + 110 | 'npm run test && npm run lint || exit 1', 111 | { mode: 511 }, 112 | ], 113 | ]); 114 | expect(log.mock.calls.filter(filterLogs)).toEqual([ 115 | [ 116 | 'debug', 117 | '🤷 - No hooks found at:', 118 | '/home/whoiam/project/dir/node_modules/metapak-fantasia/src/_common/hooks.js', 119 | ], 120 | ]); 121 | }); 122 | 123 | test('should not run on CI', async () => { 124 | $.register( 125 | constant('ENV', { 126 | CI: 1, 127 | }), 128 | ); 129 | $.register( 130 | constant('GIT_HOOKS_DIR', '/home/whoiam/project/dir/.git/hooks'), 131 | ); 132 | 133 | importer.mockResolvedValueOnce({ 134 | default: (hooks) => { 135 | hooks['pre-commit'] = hooks['pre-commit'] || []; 136 | hooks['pre-commit'].push('npm run test && npm run lint || exit 1'); 137 | return hooks; 138 | }, 139 | }); 140 | importer.mockRejectedValueOnce(new Error('E_ERROR_1')); 141 | importer.mockResolvedValueOnce({ 142 | default: (hooks) => { 143 | hooks['pre-commit'] = hooks['pre-commit'] || []; 144 | hooks['pre-commit'].push('npm run coveralls'); 145 | return hooks; 146 | }, 147 | }); 148 | importer.mockRejectedValueOnce(new Error('E_ERROR_2')); 149 | readFileAsync.mockResolvedValueOnce(Buffer.from('')); 150 | writeFileAsync.mockResolvedValueOnce(); 151 | 152 | const { buildPackageGitHooks } = await $.run<{ 153 | buildPackageGitHooks: BuildPackageGitHooksService; 154 | }>(['buildPackageGitHooks']); 155 | await buildPackageGitHooks( 156 | { 157 | metapak: { 158 | configs: ['_common'], 159 | data: {}, 160 | }, 161 | }, 162 | { 163 | configsSequence: ['_common'], 164 | modulesSequence: ['metapak-nfroidure', 'metapak-fantasia'], 165 | modulesConfigs: { 166 | 'metapak-nfroidure': { 167 | base: '/home/whoiam/project/dir/node_modules/metapak-nfroidure', 168 | srcDir: 'src', 169 | assetsDir: 'src', 170 | configs: ['_common', 'lol'], 171 | }, 172 | 'metapak-fantasia': { 173 | base: '/home/whoiam/project/dir/node_modules/metapak-fantasia', 174 | srcDir: 'src', 175 | assetsDir: 'src', 176 | configs: ['_common', 'test'], 177 | }, 178 | }, 179 | }, 180 | ); 181 | expect(importer.mock.calls).toEqual([]); 182 | expect(writeFileAsync.mock.calls).toEqual([]); 183 | expect(log.mock.calls.filter(filterLogs)).toEqual([]); 184 | }); 185 | 186 | test('should not run on parent git repository', async () => { 187 | $.register(constant('ENV', {})); 188 | $.register(constant('GIT_HOOKS_DIR', '/home/whoiam/project/.git/hooks')); 189 | 190 | importer.mockResolvedValueOnce({ 191 | default: (hooks) => { 192 | hooks['pre-commit'] = hooks['pre-commit'] || []; 193 | hooks['pre-commit'].push('npm run test && npm run lint || exit 1'); 194 | return hooks; 195 | }, 196 | }); 197 | importer.mockRejectedValueOnce(new Error('E_ERROR_1')); 198 | importer.mockResolvedValueOnce({ 199 | default: (hooks) => { 200 | hooks['pre-commit'] = hooks['pre-commit'] || []; 201 | hooks['pre-commit'].push('npm run coveralls'); 202 | return hooks; 203 | }, 204 | }); 205 | importer.mockRejectedValueOnce(new Error('E_ERROR_2')); 206 | readFileAsync.mockResolvedValueOnce(Buffer.from('')); 207 | writeFileAsync.mockResolvedValueOnce(); 208 | 209 | const { buildPackageGitHooks } = await $.run<{ 210 | buildPackageGitHooks: BuildPackageGitHooksService; 211 | }>(['buildPackageGitHooks']); 212 | await buildPackageGitHooks( 213 | { 214 | metapak: { 215 | configs: ['_common'], 216 | data: {}, 217 | }, 218 | }, 219 | { 220 | configsSequence: ['_common'], 221 | modulesSequence: ['metapak-nfroidure', 'metapak-fantasia'], 222 | modulesConfigs: { 223 | 'metapak-nfroidure': { 224 | base: '/home/whoiam/project/dir/node_modules/metapak-nfroidure', 225 | srcDir: 'src', 226 | assetsDir: 'src', 227 | configs: ['_common', 'lol'], 228 | }, 229 | 'metapak-fantasia': { 230 | base: '/home/whoiam/project/dir/node_modules/metapak-fantasia', 231 | srcDir: 'src', 232 | assetsDir: 'src', 233 | configs: ['_common', 'test'], 234 | }, 235 | }, 236 | }, 237 | ); 238 | 239 | expect(importer.mock.calls).toEqual([]); 240 | expect(writeFileAsync.mock.calls).toEqual([]); 241 | expect(log.mock.calls.filter(filterLogs)).toEqual([]); 242 | }); 243 | }); 244 | 245 | function bufferToText( 246 | call: Parameters, 247 | ): [string, string, Parameters[2]] { 248 | return [call[0], call[1].toString(), call[2]]; 249 | } 250 | 251 | function filterLogs(e: Parameters) { 252 | return !e[0].endsWith('-stack'); 253 | } 254 | -------------------------------------------------------------------------------- /src/services/gitHooks.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { autoService } from 'knifecycle'; 3 | import { mapConfigsSequentially, identity } from '../libs/utils.js'; 4 | import { YError, printStackTrace } from 'yerror'; 5 | import type { MetapakPackageJson, MetapakContext } from '../libs/utils.js'; 6 | import type { FSService } from './fs.js'; 7 | import type { ImporterService, LogService } from 'common-services'; 8 | 9 | export default autoService(initBuildPackageGitHooks); 10 | 11 | export type HooksHash = Partial< 12 | Record< 13 | | 'applypatch-msg' 14 | | 'post-update' 15 | | 'pre-commit' 16 | | 'pre-rebase' 17 | | 'commit-msg' 18 | | 'pre-applypatch' 19 | | 'prepare-commit-msg' 20 | | 'update' 21 | | 'commit-msg' 22 | | 'pre-commit' 23 | | 'pre-push', 24 | string[] 25 | > 26 | >; 27 | export type GitHooksTransformer = ( 28 | hooks: HooksHash, 29 | packageConf: MetapakPackageJson, 30 | ) => HooksHash; 31 | export type BuildPackageGitHooksService = ( 32 | packageConf: MetapakPackageJson, 33 | metapakContext: MetapakContext, 34 | ) => Promise; 35 | 36 | async function initBuildPackageGitHooks({ 37 | ENV, 38 | PROJECT_DIR, 39 | GIT_HOOKS_DIR, 40 | fs, 41 | EOL, 42 | log, 43 | importer, 44 | }: { 45 | ENV: Record; 46 | PROJECT_DIR: string; 47 | GIT_HOOKS_DIR: string; 48 | fs: FSService; 49 | EOL: string; 50 | log: LogService; 51 | importer: ImporterService<{ default: GitHooksTransformer }>; 52 | }): Promise { 53 | return async ( 54 | packageConf: MetapakPackageJson, 55 | metapakContext: MetapakContext, 56 | ): Promise => { 57 | // Avoiding CI since it does not make sense 58 | if (ENV.CI) { 59 | return; 60 | } 61 | 62 | // Avoid adding hooks for package that ain't at the git 63 | // root directory 64 | if (path.relative(PROJECT_DIR, GIT_HOOKS_DIR).startsWith('..')) { 65 | return; 66 | } 67 | 68 | const hooksBuilders = await mapConfigsSequentially( 69 | metapakContext, 70 | async ( 71 | metapakModuleName: string, 72 | metapakConfigName: string, 73 | ): Promise> => { 74 | const packageHooksPath = path.join( 75 | metapakContext.modulesConfigs[metapakModuleName].base, 76 | metapakContext.modulesConfigs[metapakModuleName].srcDir, 77 | metapakConfigName, 78 | 'hooks.js', 79 | ); 80 | 81 | try { 82 | return (await importer(packageHooksPath)).default; 83 | } catch (err) { 84 | log('debug', '🤷 - No hooks found at:', packageHooksPath); 85 | log('debug-stack', printStackTrace(err as YError)); 86 | } 87 | return identity as GitHooksTransformer; 88 | }, 89 | ); 90 | const hooks = await hooksBuilders.reduce( 91 | (hooks, hooksBuilder) => hooksBuilder(hooks, packageConf), 92 | {} as HooksHash, 93 | ); 94 | 95 | await Promise.all( 96 | Object.keys(hooks).map(async (hookName) => { 97 | const hookContent = 98 | '#!/bin/sh' + 99 | EOL + 100 | '# Automagically generated by metapak, do not change in place.' + 101 | EOL + 102 | '# Your changes would be loose on the next npm install run.' + 103 | EOL + 104 | hooks[hookName].join(';' + EOL); 105 | const hookPath = path.join(GIT_HOOKS_DIR, hookName); 106 | 107 | let currentHookContent = ''; 108 | 109 | try { 110 | currentHookContent = (await fs.readFileAsync(hookPath)).toString(); 111 | } catch (err) { 112 | log('debug', '🤷 - No existing hook found:', hookPath); 113 | log('debug-stack', printStackTrace(err as YError)); 114 | } 115 | if (currentHookContent !== hookContent) { 116 | await fs.writeFileAsync(hookPath, Buffer.from(hookContent), { 117 | mode: 0o777, 118 | }); 119 | } 120 | }), 121 | ); 122 | }; 123 | } 124 | -------------------------------------------------------------------------------- /src/services/metapak.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, test, jest, expect } from '@jest/globals'; 2 | import { Knifecycle, constant } from 'knifecycle'; 3 | import initMetapak from './metapak.js'; 4 | import { YError } from 'yerror'; 5 | import type { MetapakService } from './metapak.js'; 6 | import type { LogService, ResolveService } from 'common-services'; 7 | import type { BuildPackageConfService } from './packageConf.js'; 8 | import type { BuildPackageAssetsService } from './assets.js'; 9 | import type { BuildPackageGitHooksService } from './gitHooks.js'; 10 | import type { MetapakPackageJson } from '../libs/utils.js'; 11 | import type { FSService } from './fs.js'; 12 | 13 | describe('metapak', () => { 14 | const buildPackageConf = jest.fn(); 15 | const buildPackageAssets = jest.fn(); 16 | const buildPackageGitHooks = jest.fn(); 17 | const resolve = jest.fn( 18 | (path) => `/home/whoami/project/dir/node_modules/${path}.js`, 19 | ); 20 | const accessAsync = jest.fn(); 21 | const readFileAsync = jest.fn(); 22 | const readdirAsync = jest.fn(); 23 | const log = jest.fn(); 24 | const exit = jest.fn(); 25 | let $: Knifecycle; 26 | 27 | beforeEach(() => { 28 | log.mockReset(); 29 | exit.mockReset(); 30 | accessAsync.mockReset(); 31 | readFileAsync.mockReset(); 32 | readdirAsync.mockReset(); 33 | resolve.mockClear(); 34 | buildPackageConf.mockReset(); 35 | buildPackageAssets.mockReset(); 36 | buildPackageGitHooks.mockReset(); 37 | 38 | $ = new Knifecycle(); 39 | $.register(constant('ENV', {})); 40 | $.register(constant('log', log)); 41 | $.register(constant('exit', exit)); 42 | $.register(constant('PROJECT_DIR', 'project/dir')); 43 | $.register(constant('resolve', resolve)); 44 | $.register(constant('buildPackageConf', buildPackageConf)); 45 | $.register(constant('buildPackageAssets', buildPackageAssets)); 46 | $.register(constant('buildPackageGitHooks', buildPackageGitHooks)); 47 | $.register( 48 | constant('fs', { 49 | accessAsync, 50 | readFileAsync, 51 | readdirAsync, 52 | }), 53 | ); 54 | $.register(initMetapak); 55 | }); 56 | 57 | test('should fail with no metapak config at all', async () => { 58 | accessAsync.mockRejectedValue(new Error('E_ACCESS')); 59 | readFileAsync.mockResolvedValue(Buffer.from('{}')); 60 | buildPackageConf.mockResolvedValue(false); 61 | buildPackageAssets.mockResolvedValue(); 62 | buildPackageGitHooks.mockResolvedValue(); 63 | 64 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 65 | 66 | await metapak(); 67 | 68 | expect({ 69 | readFileAsyncCalls: readFileAsync.mock.calls, 70 | logCalls: log.mock.calls.filter(filterLogs), 71 | exitCalls: exit.mock.calls, 72 | buildPackageConfCalls: buildPackageConf.mock.calls, 73 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 74 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 75 | }).toMatchInlineSnapshot(` 76 | { 77 | "buildPackageAssetsCalls": [], 78 | "buildPackageConfCalls": [], 79 | "buildPackageGitHooksCalls": [], 80 | "exitCalls": [ 81 | [ 82 | 1, 83 | ], 84 | ], 85 | "logCalls": [ 86 | [ 87 | "error", 88 | "❌ - Metapak config not found in the project "package.json" file.", 89 | ], 90 | [ 91 | "error", 92 | "💀 - Could not run metapak script correctly:", 93 | "E_NO_METAPAK_CONFIG", 94 | [], 95 | ], 96 | [ 97 | "warning", 98 | "💊 - Debug by running again with "DEBUG=metapak" env.", 99 | ], 100 | ], 101 | "readFileAsyncCalls": [ 102 | [ 103 | "project/dir/package.json", 104 | ], 105 | ], 106 | } 107 | `); 108 | }); 109 | 110 | test('should silently fail with no metapak module', async () => { 111 | accessAsync.mockRejectedValue(new Error('E_ACCESS')); 112 | readFileAsync.mockResolvedValue( 113 | Buffer.from( 114 | JSON.stringify({ 115 | metapak: { 116 | configs: [], 117 | data: {}, 118 | }, 119 | } as MetapakPackageJson), 120 | ), 121 | ); 122 | buildPackageConf.mockResolvedValue(false); 123 | buildPackageAssets.mockResolvedValue(); 124 | buildPackageGitHooks.mockResolvedValue(); 125 | 126 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 127 | 128 | await metapak(); 129 | 130 | expect({ 131 | readFileAsyncCalls: readFileAsync.mock.calls, 132 | logCalls: log.mock.calls.filter(filterLogs), 133 | exitCalls: exit.mock.calls, 134 | buildPackageConfCalls: buildPackageConf.mock.calls, 135 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 136 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 137 | }).toMatchInlineSnapshot(` 138 | { 139 | "buildPackageAssetsCalls": [ 140 | [ 141 | { 142 | "metapak": { 143 | "configs": [], 144 | "data": {}, 145 | }, 146 | }, 147 | { 148 | "configsSequence": [], 149 | "modulesConfigs": {}, 150 | "modulesSequence": [], 151 | }, 152 | ], 153 | ], 154 | "buildPackageConfCalls": [ 155 | [ 156 | { 157 | "metapak": { 158 | "configs": [], 159 | "data": {}, 160 | }, 161 | }, 162 | { 163 | "configsSequence": [], 164 | "modulesConfigs": {}, 165 | "modulesSequence": [], 166 | }, 167 | ], 168 | ], 169 | "buildPackageGitHooksCalls": [ 170 | [ 171 | { 172 | "metapak": { 173 | "configs": [], 174 | "data": {}, 175 | }, 176 | }, 177 | { 178 | "configsSequence": [], 179 | "modulesConfigs": {}, 180 | "modulesSequence": [], 181 | }, 182 | ], 183 | ], 184 | "exitCalls": [ 185 | [ 186 | 0, 187 | ], 188 | ], 189 | "logCalls": [ 190 | [ 191 | "debug", 192 | "🤷 - No metapak modules found.", 193 | ], 194 | ], 195 | "readFileAsyncCalls": [ 196 | [ 197 | "project/dir/package.json", 198 | ], 199 | ], 200 | } 201 | `); 202 | }); 203 | 204 | test('should fail with a bad package.json path', async () => { 205 | accessAsync.mockResolvedValue(); 206 | readFileAsync.mockRejectedValue(new YError('E_AOUCH')); 207 | buildPackageConf.mockResolvedValue(false); 208 | buildPackageAssets.mockResolvedValue(); 209 | buildPackageGitHooks.mockResolvedValue(); 210 | 211 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 212 | 213 | await metapak(); 214 | 215 | expect({ 216 | readFileAsyncCalls: readFileAsync.mock.calls, 217 | logCalls: log.mock.calls.filter(filterLogs), 218 | exitCalls: exit.mock.calls, 219 | buildPackageConfCalls: buildPackageConf.mock.calls, 220 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 221 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 222 | }).toMatchInlineSnapshot(` 223 | { 224 | "buildPackageAssetsCalls": [], 225 | "buildPackageConfCalls": [], 226 | "buildPackageGitHooksCalls": [], 227 | "exitCalls": [ 228 | [ 229 | 1, 230 | ], 231 | ], 232 | "logCalls": [ 233 | [ 234 | "error", 235 | "💀 - Could not run metapak script correctly:", 236 | "E_AOUCH", 237 | [], 238 | ], 239 | [ 240 | "warning", 241 | "💊 - Debug by running again with "DEBUG=metapak" env.", 242 | ], 243 | ], 244 | "readFileAsyncCalls": [ 245 | [ 246 | "project/dir/package.json", 247 | ], 248 | ], 249 | } 250 | `); 251 | }); 252 | 253 | test('should fail with a malformed package.json', async () => { 254 | accessAsync.mockResolvedValue(); 255 | readFileAsync.mockResolvedValue(Buffer.from('{""}')); 256 | buildPackageConf.mockResolvedValue(false); 257 | buildPackageAssets.mockResolvedValue(); 258 | buildPackageGitHooks.mockResolvedValue(); 259 | 260 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 261 | 262 | await metapak(); 263 | 264 | expect({ 265 | readFileAsyncCalls: readFileAsync.mock.calls, 266 | exitCalls: exit.mock.calls, 267 | buildPackageConfCalls: buildPackageConf.mock.calls, 268 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 269 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 270 | }).toMatchInlineSnapshot(` 271 | { 272 | "buildPackageAssetsCalls": [], 273 | "buildPackageConfCalls": [], 274 | "buildPackageGitHooksCalls": [], 275 | "exitCalls": [ 276 | [ 277 | 1, 278 | ], 279 | ], 280 | "readFileAsyncCalls": [ 281 | [ 282 | "project/dir/package.json", 283 | ], 284 | ], 285 | } 286 | `); 287 | }); 288 | 289 | test('should fail with a bad sequence type', async () => { 290 | accessAsync.mockResolvedValue(); 291 | readFileAsync.mockResolvedValue( 292 | Buffer.from( 293 | JSON.stringify({ 294 | metapak: { 295 | sequence: 'unexisting_module', 296 | }, 297 | }), 298 | ), 299 | ); 300 | buildPackageConf.mockResolvedValue(false); 301 | buildPackageAssets.mockResolvedValue(); 302 | buildPackageGitHooks.mockResolvedValue(); 303 | 304 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 305 | 306 | await metapak(); 307 | 308 | expect({ 309 | readFileAsyncCalls: readFileAsync.mock.calls, 310 | logCalls: log.mock.calls.filter(filterLogs), 311 | exitCalls: exit.mock.calls, 312 | buildPackageConfCalls: buildPackageConf.mock.calls, 313 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 314 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 315 | }).toMatchInlineSnapshot(` 316 | { 317 | "buildPackageAssetsCalls": [], 318 | "buildPackageConfCalls": [], 319 | "buildPackageGitHooksCalls": [], 320 | "exitCalls": [ 321 | [ 322 | 1, 323 | ], 324 | ], 325 | "logCalls": [ 326 | [ 327 | "error", 328 | "💀 - Could not run metapak script correctly:", 329 | "E_BAD_SEQUENCE_TYPE", 330 | [ 331 | "string", 332 | "unexisting_module", 333 | ], 334 | ], 335 | [ 336 | "warning", 337 | "💊 - Debug by running again with "DEBUG=metapak" env.", 338 | ], 339 | ], 340 | "readFileAsyncCalls": [ 341 | [ 342 | "project/dir/package.json", 343 | ], 344 | ], 345 | } 346 | `); 347 | }); 348 | 349 | test('should fail with a bad sequence item', async () => { 350 | accessAsync.mockResolvedValue(); 351 | readFileAsync.mockResolvedValue( 352 | Buffer.from( 353 | JSON.stringify({ 354 | metapak: { 355 | sequence: ['unexisting_module'], 356 | configs: [], 357 | data: {}, 358 | }, 359 | } as MetapakPackageJson), 360 | ), 361 | ); 362 | buildPackageConf.mockResolvedValue(false); 363 | buildPackageAssets.mockResolvedValue(); 364 | buildPackageGitHooks.mockResolvedValue(); 365 | 366 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 367 | 368 | await metapak(); 369 | 370 | expect({ 371 | readFileAsyncCalls: readFileAsync.mock.calls, 372 | logCalls: log.mock.calls.filter(filterLogs), 373 | exitCalls: exit.mock.calls, 374 | buildPackageConfCalls: buildPackageConf.mock.calls, 375 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 376 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 377 | }).toMatchInlineSnapshot(` 378 | { 379 | "buildPackageAssetsCalls": [], 380 | "buildPackageConfCalls": [], 381 | "buildPackageGitHooksCalls": [], 382 | "exitCalls": [ 383 | [ 384 | 1, 385 | ], 386 | ], 387 | "logCalls": [ 388 | [ 389 | "error", 390 | "💀 - Could not run metapak script correctly:", 391 | "E_BAD_SEQUENCE_ITEM", 392 | [ 393 | "unexisting_module", 394 | ], 395 | ], 396 | [ 397 | "warning", 398 | "💊 - Debug by running again with "DEBUG=metapak" env.", 399 | ], 400 | ], 401 | "readFileAsyncCalls": [ 402 | [ 403 | "project/dir/package.json", 404 | ], 405 | ], 406 | } 407 | `); 408 | }); 409 | 410 | test('should fail with non-idempotent package transformer ', async () => { 411 | const packageConf: MetapakPackageJson = { 412 | metapak: { 413 | configs: ['private'], 414 | data: {}, 415 | }, 416 | devDependencies: { 417 | 'metapak-http-service': '1.0.0', 418 | }, 419 | }; 420 | 421 | readFileAsync.mockResolvedValueOnce( 422 | Buffer.from(JSON.stringify(packageConf)), 423 | ); 424 | accessAsync.mockResolvedValue(); 425 | readdirAsync.mockResolvedValue(['_common', 'private']); 426 | readFileAsync.mockResolvedValueOnce( 427 | Buffer.from( 428 | JSON.stringify({ 429 | dependencies: { 430 | siso: '1.0.0', 431 | 'strict-qs': '1.0.0', 432 | }, 433 | }), 434 | ), 435 | ); 436 | readFileAsync.mockResolvedValueOnce( 437 | Buffer.from( 438 | JSON.stringify({ 439 | private: true, 440 | }), 441 | ), 442 | ); 443 | buildPackageConf.mockResolvedValue(true); 444 | buildPackageAssets.mockResolvedValue(); 445 | buildPackageGitHooks.mockResolvedValue(); 446 | resolve.mockReturnValueOnce( 447 | 'file:///home/whoami/project/node_modules/meta-http-service/package.json', 448 | ); 449 | 450 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 451 | 452 | await metapak(); 453 | 454 | expect({ 455 | readFileAsyncCalls: readFileAsync.mock.calls, 456 | logCalls: log.mock.calls.filter(filterLogs), 457 | exitCalls: exit.mock.calls, 458 | buildPackageConfCalls: buildPackageConf.mock.calls, 459 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 460 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 461 | }).toMatchInlineSnapshot(` 462 | { 463 | "buildPackageAssetsCalls": [], 464 | "buildPackageConfCalls": [ 465 | [ 466 | { 467 | "devDependencies": { 468 | "metapak-http-service": "1.0.0", 469 | }, 470 | "metapak": { 471 | "configs": [ 472 | "private", 473 | ], 474 | "data": {}, 475 | }, 476 | }, 477 | { 478 | "configsSequence": [ 479 | "private", 480 | ], 481 | "modulesConfigs": { 482 | "metapak-http-service": { 483 | "assetsDir": "src", 484 | "base": "/home/whoami/project/node_modules/meta-http-service", 485 | "configs": [ 486 | "_common", 487 | "private", 488 | ], 489 | "srcDir": "dist", 490 | }, 491 | }, 492 | "modulesSequence": [ 493 | "metapak-http-service", 494 | ], 495 | }, 496 | ], 497 | [ 498 | { 499 | "devDependencies": { 500 | "metapak-http-service": "1.0.0", 501 | }, 502 | "metapak": { 503 | "configs": [ 504 | "private", 505 | ], 506 | "data": {}, 507 | }, 508 | }, 509 | { 510 | "configsSequence": [ 511 | "private", 512 | ], 513 | "modulesConfigs": { 514 | "metapak-http-service": { 515 | "assetsDir": "src", 516 | "base": "/home/whoami/project/node_modules/meta-http-service", 517 | "configs": [ 518 | "_common", 519 | "private", 520 | ], 521 | "srcDir": "dist", 522 | }, 523 | }, 524 | "modulesSequence": [ 525 | "metapak-http-service", 526 | ], 527 | }, 528 | ], 529 | [ 530 | { 531 | "devDependencies": { 532 | "metapak-http-service": "1.0.0", 533 | }, 534 | "metapak": { 535 | "configs": [ 536 | "private", 537 | ], 538 | "data": {}, 539 | }, 540 | }, 541 | { 542 | "configsSequence": [ 543 | "private", 544 | ], 545 | "modulesConfigs": { 546 | "metapak-http-service": { 547 | "assetsDir": "src", 548 | "base": "/home/whoami/project/node_modules/meta-http-service", 549 | "configs": [ 550 | "_common", 551 | "private", 552 | ], 553 | "srcDir": "dist", 554 | }, 555 | }, 556 | "modulesSequence": [ 557 | "metapak-http-service", 558 | ], 559 | }, 560 | ], 561 | [ 562 | { 563 | "devDependencies": { 564 | "metapak-http-service": "1.0.0", 565 | }, 566 | "metapak": { 567 | "configs": [ 568 | "private", 569 | ], 570 | "data": {}, 571 | }, 572 | }, 573 | { 574 | "configsSequence": [ 575 | "private", 576 | ], 577 | "modulesConfigs": { 578 | "metapak-http-service": { 579 | "assetsDir": "src", 580 | "base": "/home/whoami/project/node_modules/meta-http-service", 581 | "configs": [ 582 | "_common", 583 | "private", 584 | ], 585 | "srcDir": "dist", 586 | }, 587 | }, 588 | "modulesSequence": [ 589 | "metapak-http-service", 590 | ], 591 | }, 592 | ], 593 | [ 594 | { 595 | "devDependencies": { 596 | "metapak-http-service": "1.0.0", 597 | }, 598 | "metapak": { 599 | "configs": [ 600 | "private", 601 | ], 602 | "data": {}, 603 | }, 604 | }, 605 | { 606 | "configsSequence": [ 607 | "private", 608 | ], 609 | "modulesConfigs": { 610 | "metapak-http-service": { 611 | "assetsDir": "src", 612 | "base": "/home/whoami/project/node_modules/meta-http-service", 613 | "configs": [ 614 | "_common", 615 | "private", 616 | ], 617 | "srcDir": "dist", 618 | }, 619 | }, 620 | "modulesSequence": [ 621 | "metapak-http-service", 622 | ], 623 | }, 624 | ], 625 | [ 626 | { 627 | "devDependencies": { 628 | "metapak-http-service": "1.0.0", 629 | }, 630 | "metapak": { 631 | "configs": [ 632 | "private", 633 | ], 634 | "data": {}, 635 | }, 636 | }, 637 | { 638 | "configsSequence": [ 639 | "private", 640 | ], 641 | "modulesConfigs": { 642 | "metapak-http-service": { 643 | "assetsDir": "src", 644 | "base": "/home/whoami/project/node_modules/meta-http-service", 645 | "configs": [ 646 | "_common", 647 | "private", 648 | ], 649 | "srcDir": "dist", 650 | }, 651 | }, 652 | "modulesSequence": [ 653 | "metapak-http-service", 654 | ], 655 | }, 656 | ], 657 | [ 658 | { 659 | "devDependencies": { 660 | "metapak-http-service": "1.0.0", 661 | }, 662 | "metapak": { 663 | "configs": [ 664 | "private", 665 | ], 666 | "data": {}, 667 | }, 668 | }, 669 | { 670 | "configsSequence": [ 671 | "private", 672 | ], 673 | "modulesConfigs": { 674 | "metapak-http-service": { 675 | "assetsDir": "src", 676 | "base": "/home/whoami/project/node_modules/meta-http-service", 677 | "configs": [ 678 | "_common", 679 | "private", 680 | ], 681 | "srcDir": "dist", 682 | }, 683 | }, 684 | "modulesSequence": [ 685 | "metapak-http-service", 686 | ], 687 | }, 688 | ], 689 | [ 690 | { 691 | "devDependencies": { 692 | "metapak-http-service": "1.0.0", 693 | }, 694 | "metapak": { 695 | "configs": [ 696 | "private", 697 | ], 698 | "data": {}, 699 | }, 700 | }, 701 | { 702 | "configsSequence": [ 703 | "private", 704 | ], 705 | "modulesConfigs": { 706 | "metapak-http-service": { 707 | "assetsDir": "src", 708 | "base": "/home/whoami/project/node_modules/meta-http-service", 709 | "configs": [ 710 | "_common", 711 | "private", 712 | ], 713 | "srcDir": "dist", 714 | }, 715 | }, 716 | "modulesSequence": [ 717 | "metapak-http-service", 718 | ], 719 | }, 720 | ], 721 | [ 722 | { 723 | "devDependencies": { 724 | "metapak-http-service": "1.0.0", 725 | }, 726 | "metapak": { 727 | "configs": [ 728 | "private", 729 | ], 730 | "data": {}, 731 | }, 732 | }, 733 | { 734 | "configsSequence": [ 735 | "private", 736 | ], 737 | "modulesConfigs": { 738 | "metapak-http-service": { 739 | "assetsDir": "src", 740 | "base": "/home/whoami/project/node_modules/meta-http-service", 741 | "configs": [ 742 | "_common", 743 | "private", 744 | ], 745 | "srcDir": "dist", 746 | }, 747 | }, 748 | "modulesSequence": [ 749 | "metapak-http-service", 750 | ], 751 | }, 752 | ], 753 | [ 754 | { 755 | "devDependencies": { 756 | "metapak-http-service": "1.0.0", 757 | }, 758 | "metapak": { 759 | "configs": [ 760 | "private", 761 | ], 762 | "data": {}, 763 | }, 764 | }, 765 | { 766 | "configsSequence": [ 767 | "private", 768 | ], 769 | "modulesConfigs": { 770 | "metapak-http-service": { 771 | "assetsDir": "src", 772 | "base": "/home/whoami/project/node_modules/meta-http-service", 773 | "configs": [ 774 | "_common", 775 | "private", 776 | ], 777 | "srcDir": "dist", 778 | }, 779 | }, 780 | "modulesSequence": [ 781 | "metapak-http-service", 782 | ], 783 | }, 784 | ], 785 | [ 786 | { 787 | "devDependencies": { 788 | "metapak-http-service": "1.0.0", 789 | }, 790 | "metapak": { 791 | "configs": [ 792 | "private", 793 | ], 794 | "data": {}, 795 | }, 796 | }, 797 | { 798 | "configsSequence": [ 799 | "private", 800 | ], 801 | "modulesConfigs": { 802 | "metapak-http-service": { 803 | "assetsDir": "src", 804 | "base": "/home/whoami/project/node_modules/meta-http-service", 805 | "configs": [ 806 | "_common", 807 | "private", 808 | ], 809 | "srcDir": "dist", 810 | }, 811 | }, 812 | "modulesSequence": [ 813 | "metapak-http-service", 814 | ], 815 | }, 816 | ], 817 | [ 818 | { 819 | "devDependencies": { 820 | "metapak-http-service": "1.0.0", 821 | }, 822 | "metapak": { 823 | "configs": [ 824 | "private", 825 | ], 826 | "data": {}, 827 | }, 828 | }, 829 | { 830 | "configsSequence": [ 831 | "private", 832 | ], 833 | "modulesConfigs": { 834 | "metapak-http-service": { 835 | "assetsDir": "src", 836 | "base": "/home/whoami/project/node_modules/meta-http-service", 837 | "configs": [ 838 | "_common", 839 | "private", 840 | ], 841 | "srcDir": "dist", 842 | }, 843 | }, 844 | "modulesSequence": [ 845 | "metapak-http-service", 846 | ], 847 | }, 848 | ], 849 | [ 850 | { 851 | "devDependencies": { 852 | "metapak-http-service": "1.0.0", 853 | }, 854 | "metapak": { 855 | "configs": [ 856 | "private", 857 | ], 858 | "data": {}, 859 | }, 860 | }, 861 | { 862 | "configsSequence": [ 863 | "private", 864 | ], 865 | "modulesConfigs": { 866 | "metapak-http-service": { 867 | "assetsDir": "src", 868 | "base": "/home/whoami/project/node_modules/meta-http-service", 869 | "configs": [ 870 | "_common", 871 | "private", 872 | ], 873 | "srcDir": "dist", 874 | }, 875 | }, 876 | "modulesSequence": [ 877 | "metapak-http-service", 878 | ], 879 | }, 880 | ], 881 | [ 882 | { 883 | "devDependencies": { 884 | "metapak-http-service": "1.0.0", 885 | }, 886 | "metapak": { 887 | "configs": [ 888 | "private", 889 | ], 890 | "data": {}, 891 | }, 892 | }, 893 | { 894 | "configsSequence": [ 895 | "private", 896 | ], 897 | "modulesConfigs": { 898 | "metapak-http-service": { 899 | "assetsDir": "src", 900 | "base": "/home/whoami/project/node_modules/meta-http-service", 901 | "configs": [ 902 | "_common", 903 | "private", 904 | ], 905 | "srcDir": "dist", 906 | }, 907 | }, 908 | "modulesSequence": [ 909 | "metapak-http-service", 910 | ], 911 | }, 912 | ], 913 | [ 914 | { 915 | "devDependencies": { 916 | "metapak-http-service": "1.0.0", 917 | }, 918 | "metapak": { 919 | "configs": [ 920 | "private", 921 | ], 922 | "data": {}, 923 | }, 924 | }, 925 | { 926 | "configsSequence": [ 927 | "private", 928 | ], 929 | "modulesConfigs": { 930 | "metapak-http-service": { 931 | "assetsDir": "src", 932 | "base": "/home/whoami/project/node_modules/meta-http-service", 933 | "configs": [ 934 | "_common", 935 | "private", 936 | ], 937 | "srcDir": "dist", 938 | }, 939 | }, 940 | "modulesSequence": [ 941 | "metapak-http-service", 942 | ], 943 | }, 944 | ], 945 | ], 946 | "buildPackageGitHooksCalls": [], 947 | "exitCalls": [ 948 | [ 949 | 1, 950 | ], 951 | ], 952 | "logCalls": [ 953 | [ 954 | "debug", 955 | "✅ - Resolved the metapak modules sequence:", 956 | [ 957 | "metapak-http-service", 958 | ], 959 | ], 960 | [ 961 | "debug", 962 | "📥 - Built config for "metapak-http-service:", 963 | { 964 | "assetsDir": "src", 965 | "base": "/home/whoami/project/node_modules/meta-http-service", 966 | "configs": [ 967 | "_common", 968 | "private", 969 | ], 970 | "srcDir": "dist", 971 | }, 972 | ], 973 | [ 974 | "error", 975 | "🤷 - Reached the maximum allowed iterations. It means metapak keeps changing the repository and never reach a stable state. Probably that some operations made are not idempotent.", 976 | ], 977 | [ 978 | "error", 979 | "💀 - Could not run metapak script correctly:", 980 | "E_MAX_ITERATIONS", 981 | [ 982 | 15, 983 | 15, 984 | ], 985 | ], 986 | [ 987 | "warning", 988 | "💊 - Debug by running again with "DEBUG=metapak" env.", 989 | ], 990 | ], 991 | "readFileAsyncCalls": [ 992 | [ 993 | "project/dir/package.json", 994 | ], 995 | ], 996 | } 997 | `); 998 | }); 999 | 1000 | test('should work with one module and several configs', async () => { 1001 | const packageConf: MetapakPackageJson = { 1002 | metapak: { 1003 | configs: ['_common', 'private'], 1004 | data: {}, 1005 | }, 1006 | devDependencies: { 1007 | 'metapak-http-service': '1.0.0', 1008 | }, 1009 | }; 1010 | 1011 | readFileAsync.mockResolvedValueOnce( 1012 | Buffer.from(JSON.stringify(packageConf)), 1013 | ); 1014 | accessAsync.mockResolvedValue(); 1015 | readdirAsync.mockResolvedValue(['_common', 'private']); 1016 | readFileAsync.mockResolvedValueOnce( 1017 | Buffer.from( 1018 | JSON.stringify({ 1019 | dependencies: { 1020 | siso: '1.0.0', 1021 | 'strict-qs': '1.0.0', 1022 | }, 1023 | }), 1024 | ), 1025 | ); 1026 | readFileAsync.mockResolvedValueOnce( 1027 | Buffer.from( 1028 | JSON.stringify({ 1029 | private: true, 1030 | }), 1031 | ), 1032 | ); 1033 | buildPackageConf.mockResolvedValue(false); 1034 | buildPackageAssets.mockResolvedValue(); 1035 | buildPackageGitHooks.mockResolvedValue(); 1036 | resolve.mockReturnValueOnce( 1037 | 'file:///home/whoami/project/node_modules/meta-http-service/package.json', 1038 | ); 1039 | 1040 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 1041 | 1042 | await metapak(); 1043 | 1044 | expect({ 1045 | readFileAsyncCalls: readFileAsync.mock.calls, 1046 | logCalls: log.mock.calls.filter(filterLogs), 1047 | exitCalls: exit.mock.calls, 1048 | buildPackageConfCalls: buildPackageConf.mock.calls, 1049 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 1050 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 1051 | }).toMatchInlineSnapshot(` 1052 | { 1053 | "buildPackageAssetsCalls": [ 1054 | [ 1055 | { 1056 | "devDependencies": { 1057 | "metapak-http-service": "1.0.0", 1058 | }, 1059 | "metapak": { 1060 | "configs": [ 1061 | "_common", 1062 | "private", 1063 | ], 1064 | "data": {}, 1065 | }, 1066 | }, 1067 | { 1068 | "configsSequence": [ 1069 | "_common", 1070 | "private", 1071 | ], 1072 | "modulesConfigs": { 1073 | "metapak-http-service": { 1074 | "assetsDir": "src", 1075 | "base": "/home/whoami/project/node_modules/meta-http-service", 1076 | "configs": [ 1077 | "_common", 1078 | "private", 1079 | ], 1080 | "srcDir": "dist", 1081 | }, 1082 | }, 1083 | "modulesSequence": [ 1084 | "metapak-http-service", 1085 | ], 1086 | }, 1087 | ], 1088 | ], 1089 | "buildPackageConfCalls": [ 1090 | [ 1091 | { 1092 | "devDependencies": { 1093 | "metapak-http-service": "1.0.0", 1094 | }, 1095 | "metapak": { 1096 | "configs": [ 1097 | "_common", 1098 | "private", 1099 | ], 1100 | "data": {}, 1101 | }, 1102 | }, 1103 | { 1104 | "configsSequence": [ 1105 | "_common", 1106 | "private", 1107 | ], 1108 | "modulesConfigs": { 1109 | "metapak-http-service": { 1110 | "assetsDir": "src", 1111 | "base": "/home/whoami/project/node_modules/meta-http-service", 1112 | "configs": [ 1113 | "_common", 1114 | "private", 1115 | ], 1116 | "srcDir": "dist", 1117 | }, 1118 | }, 1119 | "modulesSequence": [ 1120 | "metapak-http-service", 1121 | ], 1122 | }, 1123 | ], 1124 | ], 1125 | "buildPackageGitHooksCalls": [ 1126 | [ 1127 | { 1128 | "devDependencies": { 1129 | "metapak-http-service": "1.0.0", 1130 | }, 1131 | "metapak": { 1132 | "configs": [ 1133 | "_common", 1134 | "private", 1135 | ], 1136 | "data": {}, 1137 | }, 1138 | }, 1139 | { 1140 | "configsSequence": [ 1141 | "_common", 1142 | "private", 1143 | ], 1144 | "modulesConfigs": { 1145 | "metapak-http-service": { 1146 | "assetsDir": "src", 1147 | "base": "/home/whoami/project/node_modules/meta-http-service", 1148 | "configs": [ 1149 | "_common", 1150 | "private", 1151 | ], 1152 | "srcDir": "dist", 1153 | }, 1154 | }, 1155 | "modulesSequence": [ 1156 | "metapak-http-service", 1157 | ], 1158 | }, 1159 | ], 1160 | ], 1161 | "exitCalls": [ 1162 | [ 1163 | 0, 1164 | ], 1165 | ], 1166 | "logCalls": [ 1167 | [ 1168 | "debug", 1169 | "✅ - Resolved the metapak modules sequence:", 1170 | [ 1171 | "metapak-http-service", 1172 | ], 1173 | ], 1174 | [ 1175 | "debug", 1176 | "📥 - Built config for "metapak-http-service:", 1177 | { 1178 | "assetsDir": "src", 1179 | "base": "/home/whoami/project/node_modules/meta-http-service", 1180 | "configs": [ 1181 | "_common", 1182 | "private", 1183 | ], 1184 | "srcDir": "dist", 1185 | }, 1186 | ], 1187 | ], 1188 | "readFileAsyncCalls": [ 1189 | [ 1190 | "project/dir/package.json", 1191 | ], 1192 | ], 1193 | } 1194 | `); 1195 | }); 1196 | 1197 | test('should work with one module and one config', async () => { 1198 | const packageConf: MetapakPackageJson = { 1199 | devDependencies: { 1200 | 'metapak-http-service': '1.0.0', 1201 | }, 1202 | metapak: { 1203 | configs: ['private'], 1204 | data: {}, 1205 | }, 1206 | }; 1207 | 1208 | readFileAsync.mockResolvedValueOnce( 1209 | Buffer.from(JSON.stringify(packageConf)), 1210 | ); 1211 | accessAsync.mockResolvedValue(); 1212 | readdirAsync.mockResolvedValue(['_common', 'private']); 1213 | readFileAsync.mockResolvedValueOnce( 1214 | Buffer.from( 1215 | JSON.stringify({ 1216 | dependencies: { 1217 | siso: '1.0.0', 1218 | 'strict-qs': '1.0.0', 1219 | }, 1220 | }), 1221 | ), 1222 | ); 1223 | readFileAsync.mockResolvedValueOnce( 1224 | Buffer.from( 1225 | JSON.stringify({ 1226 | private: true, 1227 | }), 1228 | ), 1229 | ); 1230 | buildPackageConf.mockResolvedValue(false); 1231 | buildPackageAssets.mockResolvedValue(); 1232 | buildPackageGitHooks.mockResolvedValue(); 1233 | resolve.mockReturnValueOnce( 1234 | 'file:///home/whoami/project/node_modules/meta-http-service/package.json', 1235 | ); 1236 | 1237 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 1238 | 1239 | await metapak(); 1240 | 1241 | expect({ 1242 | readFileAsyncCalls: readFileAsync.mock.calls, 1243 | logCalls: log.mock.calls.filter(filterLogs), 1244 | exitCalls: exit.mock.calls, 1245 | buildPackageConfCalls: buildPackageConf.mock.calls, 1246 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 1247 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 1248 | }).toMatchInlineSnapshot(` 1249 | { 1250 | "buildPackageAssetsCalls": [ 1251 | [ 1252 | { 1253 | "devDependencies": { 1254 | "metapak-http-service": "1.0.0", 1255 | }, 1256 | "metapak": { 1257 | "configs": [ 1258 | "private", 1259 | ], 1260 | "data": {}, 1261 | }, 1262 | }, 1263 | { 1264 | "configsSequence": [ 1265 | "private", 1266 | ], 1267 | "modulesConfigs": { 1268 | "metapak-http-service": { 1269 | "assetsDir": "src", 1270 | "base": "/home/whoami/project/node_modules/meta-http-service", 1271 | "configs": [ 1272 | "_common", 1273 | "private", 1274 | ], 1275 | "srcDir": "dist", 1276 | }, 1277 | }, 1278 | "modulesSequence": [ 1279 | "metapak-http-service", 1280 | ], 1281 | }, 1282 | ], 1283 | ], 1284 | "buildPackageConfCalls": [ 1285 | [ 1286 | { 1287 | "devDependencies": { 1288 | "metapak-http-service": "1.0.0", 1289 | }, 1290 | "metapak": { 1291 | "configs": [ 1292 | "private", 1293 | ], 1294 | "data": {}, 1295 | }, 1296 | }, 1297 | { 1298 | "configsSequence": [ 1299 | "private", 1300 | ], 1301 | "modulesConfigs": { 1302 | "metapak-http-service": { 1303 | "assetsDir": "src", 1304 | "base": "/home/whoami/project/node_modules/meta-http-service", 1305 | "configs": [ 1306 | "_common", 1307 | "private", 1308 | ], 1309 | "srcDir": "dist", 1310 | }, 1311 | }, 1312 | "modulesSequence": [ 1313 | "metapak-http-service", 1314 | ], 1315 | }, 1316 | ], 1317 | ], 1318 | "buildPackageGitHooksCalls": [ 1319 | [ 1320 | { 1321 | "devDependencies": { 1322 | "metapak-http-service": "1.0.0", 1323 | }, 1324 | "metapak": { 1325 | "configs": [ 1326 | "private", 1327 | ], 1328 | "data": {}, 1329 | }, 1330 | }, 1331 | { 1332 | "configsSequence": [ 1333 | "private", 1334 | ], 1335 | "modulesConfigs": { 1336 | "metapak-http-service": { 1337 | "assetsDir": "src", 1338 | "base": "/home/whoami/project/node_modules/meta-http-service", 1339 | "configs": [ 1340 | "_common", 1341 | "private", 1342 | ], 1343 | "srcDir": "dist", 1344 | }, 1345 | }, 1346 | "modulesSequence": [ 1347 | "metapak-http-service", 1348 | ], 1349 | }, 1350 | ], 1351 | ], 1352 | "exitCalls": [ 1353 | [ 1354 | 0, 1355 | ], 1356 | ], 1357 | "logCalls": [ 1358 | [ 1359 | "debug", 1360 | "✅ - Resolved the metapak modules sequence:", 1361 | [ 1362 | "metapak-http-service", 1363 | ], 1364 | ], 1365 | [ 1366 | "debug", 1367 | "📥 - Built config for "metapak-http-service:", 1368 | { 1369 | "assetsDir": "src", 1370 | "base": "/home/whoami/project/node_modules/meta-http-service", 1371 | "configs": [ 1372 | "_common", 1373 | "private", 1374 | ], 1375 | "srcDir": "dist", 1376 | }, 1377 | ], 1378 | ], 1379 | "readFileAsyncCalls": [ 1380 | [ 1381 | "project/dir/package.json", 1382 | ], 1383 | ], 1384 | } 1385 | `); 1386 | }); 1387 | 1388 | test('should work with one module and several overriden configs', async () => { 1389 | const packageConf: MetapakPackageJson = { 1390 | devDependencies: { 1391 | 'metapak-http-service': '1.0.0', 1392 | }, 1393 | metapak: { 1394 | configs: ['private', 'bisous'], 1395 | data: {}, 1396 | }, 1397 | }; 1398 | 1399 | readFileAsync.mockResolvedValueOnce( 1400 | Buffer.from(JSON.stringify(packageConf)), 1401 | ); 1402 | accessAsync.mockResolvedValue(); 1403 | readdirAsync.mockResolvedValue(['_common', 'bisous', 'private', 'coucou']); 1404 | readFileAsync.mockResolvedValueOnce( 1405 | Buffer.from( 1406 | JSON.stringify({ 1407 | dependencies: { 1408 | siso: '1.0.0', 1409 | 'strict-qs': '1.0.0', 1410 | }, 1411 | }), 1412 | ), 1413 | ); 1414 | readFileAsync.mockResolvedValueOnce( 1415 | Buffer.from( 1416 | JSON.stringify({ 1417 | private: true, 1418 | }), 1419 | ), 1420 | ); 1421 | buildPackageConf.mockResolvedValue(false); 1422 | buildPackageAssets.mockResolvedValue(); 1423 | buildPackageGitHooks.mockResolvedValue(); 1424 | resolve.mockReturnValueOnce( 1425 | 'file:///home/whoami/project/node_modules/meta-http-service/package.json', 1426 | ); 1427 | 1428 | const { metapak } = await $.run<{ metapak: MetapakService }>(['metapak']); 1429 | 1430 | await metapak(); 1431 | 1432 | expect({ 1433 | readFileAsyncCalls: readFileAsync.mock.calls, 1434 | logCalls: log.mock.calls.filter(filterLogs), 1435 | exitCalls: exit.mock.calls, 1436 | buildPackageConfCalls: buildPackageConf.mock.calls, 1437 | buildPackageAssetsCalls: buildPackageAssets.mock.calls, 1438 | buildPackageGitHooksCalls: buildPackageGitHooks.mock.calls, 1439 | }).toMatchInlineSnapshot(` 1440 | { 1441 | "buildPackageAssetsCalls": [ 1442 | [ 1443 | { 1444 | "devDependencies": { 1445 | "metapak-http-service": "1.0.0", 1446 | }, 1447 | "metapak": { 1448 | "configs": [ 1449 | "private", 1450 | "bisous", 1451 | ], 1452 | "data": {}, 1453 | }, 1454 | }, 1455 | { 1456 | "configsSequence": [ 1457 | "private", 1458 | "bisous", 1459 | ], 1460 | "modulesConfigs": { 1461 | "metapak-http-service": { 1462 | "assetsDir": "src", 1463 | "base": "/home/whoami/project/node_modules/meta-http-service", 1464 | "configs": [ 1465 | "_common", 1466 | "bisous", 1467 | "private", 1468 | "coucou", 1469 | ], 1470 | "srcDir": "dist", 1471 | }, 1472 | }, 1473 | "modulesSequence": [ 1474 | "metapak-http-service", 1475 | ], 1476 | }, 1477 | ], 1478 | ], 1479 | "buildPackageConfCalls": [ 1480 | [ 1481 | { 1482 | "devDependencies": { 1483 | "metapak-http-service": "1.0.0", 1484 | }, 1485 | "metapak": { 1486 | "configs": [ 1487 | "private", 1488 | "bisous", 1489 | ], 1490 | "data": {}, 1491 | }, 1492 | }, 1493 | { 1494 | "configsSequence": [ 1495 | "private", 1496 | "bisous", 1497 | ], 1498 | "modulesConfigs": { 1499 | "metapak-http-service": { 1500 | "assetsDir": "src", 1501 | "base": "/home/whoami/project/node_modules/meta-http-service", 1502 | "configs": [ 1503 | "_common", 1504 | "bisous", 1505 | "private", 1506 | "coucou", 1507 | ], 1508 | "srcDir": "dist", 1509 | }, 1510 | }, 1511 | "modulesSequence": [ 1512 | "metapak-http-service", 1513 | ], 1514 | }, 1515 | ], 1516 | ], 1517 | "buildPackageGitHooksCalls": [ 1518 | [ 1519 | { 1520 | "devDependencies": { 1521 | "metapak-http-service": "1.0.0", 1522 | }, 1523 | "metapak": { 1524 | "configs": [ 1525 | "private", 1526 | "bisous", 1527 | ], 1528 | "data": {}, 1529 | }, 1530 | }, 1531 | { 1532 | "configsSequence": [ 1533 | "private", 1534 | "bisous", 1535 | ], 1536 | "modulesConfigs": { 1537 | "metapak-http-service": { 1538 | "assetsDir": "src", 1539 | "base": "/home/whoami/project/node_modules/meta-http-service", 1540 | "configs": [ 1541 | "_common", 1542 | "bisous", 1543 | "private", 1544 | "coucou", 1545 | ], 1546 | "srcDir": "dist", 1547 | }, 1548 | }, 1549 | "modulesSequence": [ 1550 | "metapak-http-service", 1551 | ], 1552 | }, 1553 | ], 1554 | ], 1555 | "exitCalls": [ 1556 | [ 1557 | 0, 1558 | ], 1559 | ], 1560 | "logCalls": [ 1561 | [ 1562 | "debug", 1563 | "✅ - Resolved the metapak modules sequence:", 1564 | [ 1565 | "metapak-http-service", 1566 | ], 1567 | ], 1568 | [ 1569 | "debug", 1570 | "📥 - Built config for "metapak-http-service:", 1571 | { 1572 | "assetsDir": "src", 1573 | "base": "/home/whoami/project/node_modules/meta-http-service", 1574 | "configs": [ 1575 | "_common", 1576 | "bisous", 1577 | "private", 1578 | "coucou", 1579 | ], 1580 | "srcDir": "dist", 1581 | }, 1582 | ], 1583 | ], 1584 | "readFileAsyncCalls": [ 1585 | [ 1586 | "project/dir/package.json", 1587 | ], 1588 | ], 1589 | } 1590 | `); 1591 | }); 1592 | }); 1593 | 1594 | function filterLogs(e: Parameters) { 1595 | return !e[0].endsWith('-stack'); 1596 | } 1597 | -------------------------------------------------------------------------------- /src/services/metapak.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { printStackTrace, YError } from 'yerror'; 4 | import { autoService } from 'knifecycle'; 5 | import type { 6 | MetapakContext, 7 | MetapakModuleConfigs, 8 | MetapakPackageJson, 9 | } from '../libs/utils.js'; 10 | import type { LogService, ResolveService } from 'common-services'; 11 | import type { FSService } from './fs.js'; 12 | import type { BuildPackageAssetsService } from './assets.js'; 13 | import type { BuildPackageGitHooksService } from './gitHooks.js'; 14 | import type { BuildPackageConfService } from './packageConf.js'; 15 | 16 | export type MetapakService = () => Promise; 17 | 18 | const MAX_PACKAGE_BUILD_ITERATIONS = 15; 19 | 20 | export default autoService(initMetapak); 21 | 22 | async function initMetapak({ 23 | ENV, 24 | PROJECT_DIR, 25 | log, 26 | exit, 27 | fs, 28 | buildPackageConf, 29 | buildPackageAssets, 30 | buildPackageGitHooks, 31 | resolve, 32 | }: { 33 | ENV: Record; 34 | PROJECT_DIR: string; 35 | log: LogService; 36 | exit: typeof process.exit; 37 | fs: FSService; 38 | buildPackageConf: BuildPackageConfService; 39 | buildPackageAssets: BuildPackageAssetsService; 40 | buildPackageGitHooks: BuildPackageGitHooksService; 41 | resolve: ResolveService; 42 | }): Promise { 43 | return async function metapak() { 44 | try { 45 | const basePackageConf = JSON.parse( 46 | (await fs.readFileAsync(join(PROJECT_DIR, 'package.json'))).toString(), 47 | ); 48 | 49 | if (!('metapak' in basePackageConf)) { 50 | log( 51 | 'error', 52 | `❌ - Metapak config not found in the project "package.json" file.`, 53 | ); 54 | throw new YError('E_NO_METAPAK_CONFIG'); 55 | } 56 | 57 | const packageConf = { 58 | metapak: { 59 | data: {}, 60 | config: [], 61 | ...(basePackageConf.metapak || {}), 62 | }, 63 | ...basePackageConf, 64 | } as MetapakPackageJson; 65 | 66 | const metapakModulesSequence = _getMetapakModulesSequence( 67 | { log }, 68 | packageConf, 69 | ); 70 | 71 | if (!metapakModulesSequence.length) { 72 | log('debug', '🤷 - No metapak modules found.'); 73 | } else { 74 | log( 75 | 'debug', 76 | '✅ - Resolved the metapak modules sequence:', 77 | metapakModulesSequence, 78 | ); 79 | } 80 | 81 | const metapakModulesConfigs = await readMetapakModulesConfigs( 82 | { 83 | PROJECT_DIR, 84 | fs, 85 | log, 86 | resolve, 87 | }, 88 | metapakModulesSequence, 89 | packageConf, 90 | ); 91 | 92 | const metapakConfigsSequence = ( 93 | packageConf.metapak?.configs || [] 94 | ).filter((configName) => { 95 | const configFound = Object.keys(metapakModulesConfigs).some( 96 | (aModuleName) => 97 | metapakModulesConfigs[aModuleName].configs.includes(configName), 98 | ); 99 | 100 | if (!configFound) { 101 | log( 102 | 'error', 103 | `❌ - Metapak configs sequence refers to an unavailable config (${configName}).`, 104 | ); 105 | } 106 | 107 | return configFound; 108 | }); 109 | 110 | const metapakContext: MetapakContext = { 111 | modulesConfigs: metapakModulesConfigs, 112 | modulesSequence: metapakModulesSequence, 113 | configsSequence: metapakConfigsSequence, 114 | }; 115 | let packageConfBuild = false; 116 | let packageConfBuildResult = false; 117 | let iteration = 0; 118 | 119 | do { 120 | packageConfBuildResult = await buildPackageConf( 121 | packageConf, 122 | metapakContext, 123 | ); 124 | packageConfBuild = packageConfBuildResult || packageConfBuild; 125 | iteration++; 126 | } while ( 127 | packageConfBuildResult && 128 | iteration < MAX_PACKAGE_BUILD_ITERATIONS 129 | ); 130 | 131 | if (packageConfBuildResult) { 132 | log( 133 | 'error', 134 | `🤷 - Reached the maximum allowed iterations. It means metapak keeps changing the repository and never reach a stable state. Probably that some operations made are not idempotent.`, 135 | ); 136 | throw new YError( 137 | 'E_MAX_ITERATIONS', 138 | iteration, 139 | MAX_PACKAGE_BUILD_ITERATIONS, 140 | ); 141 | } 142 | 143 | const promises = [ 144 | Promise.resolve(packageConfBuild), 145 | buildPackageAssets(packageConf, metapakContext), 146 | buildPackageGitHooks(packageConf, metapakContext), 147 | ]; 148 | 149 | // Avoid stopping the process immediately for one failure 150 | await Promise.allSettled(promises); 151 | const [packageConfModified, assetsModified] = await Promise.all(promises); 152 | 153 | // The CI should not modify the repo contents and should fail when the 154 | // package would have been modified cause it should not happen and it probably 155 | // is a metapak misuse. 156 | if ((packageConfModified || assetsModified) && ENV.CI) { 157 | log( 158 | 'error', 159 | '💀 - This commit is not valid since it do not match the meta package state.', 160 | ); 161 | exit(1); 162 | } 163 | if (packageConfModified) { 164 | log( 165 | 'warning', 166 | '🚧 - Changed the `package.json` file, you may need to run `npm install` to get new dependencies.', 167 | ); 168 | } 169 | if (assetsModified) { 170 | log( 171 | 'warning', 172 | '🚧 - Some assets were added to the project, you may want to stage them.', 173 | ); 174 | } 175 | exit(0); 176 | } catch (err) { 177 | const castedErr = YError.cast(err as Error); 178 | 179 | log( 180 | 'error', 181 | '💀 - Could not run metapak script correctly:', 182 | castedErr.code, 183 | castedErr.params, 184 | ); 185 | log('warning', '💊 - Debug by running again with "DEBUG=metapak" env.'); 186 | log('error-stack', printStackTrace(castedErr)); 187 | exit(1); 188 | } 189 | }; 190 | } 191 | 192 | function _getMetapakModulesSequence( 193 | { log }: { log: LogService }, 194 | packageConf: MetapakPackageJson, 195 | ) { 196 | const reg = new RegExp(/^(@.+\/)?metapak-/); 197 | const metapakModulesNames = Object.keys( 198 | packageConf.devDependencies || {}, 199 | ).filter((devDependency) => reg.test(devDependency)); 200 | 201 | // Allowing a metapak module to run on himself 202 | if (packageConf.name && reg.test(packageConf.name)) { 203 | metapakModulesNames.unshift(packageConf.name); 204 | } 205 | 206 | return _reorderMetapakModulesNames({ log }, packageConf, metapakModulesNames); 207 | } 208 | 209 | function _reorderMetapakModulesNames( 210 | { log }: { log: LogService }, 211 | packageConf: MetapakPackageJson, 212 | metapakModulesNames: string[], 213 | ) { 214 | if (packageConf.metapak && packageConf.metapak.sequence) { 215 | if (!(packageConf.metapak.sequence instanceof Array)) { 216 | throw new YError( 217 | 'E_BAD_SEQUENCE_TYPE', 218 | typeof packageConf.metapak.sequence, 219 | packageConf.metapak.sequence, 220 | ); 221 | } 222 | packageConf.metapak.sequence.forEach((moduleName) => { 223 | if (!metapakModulesNames.includes(moduleName)) { 224 | throw new YError('E_BAD_SEQUENCE_ITEM', moduleName); 225 | } 226 | }); 227 | log( 228 | 'debug', 229 | '💱 - Reordering metapak modules sequence.', 230 | packageConf.metapak.sequence, 231 | ); 232 | return packageConf.metapak.sequence; 233 | } 234 | return metapakModulesNames; 235 | } 236 | 237 | async function readMetapakModulesConfigs( 238 | { 239 | PROJECT_DIR, 240 | fs, 241 | log, 242 | resolve, 243 | }: { 244 | PROJECT_DIR: string; 245 | fs: FSService; 246 | log: LogService; 247 | resolve: ResolveService; 248 | }, 249 | metapakModulesSequence: string[], 250 | packageConf: MetapakPackageJson, 251 | ): Promise { 252 | const moduleConfigs: MetapakModuleConfigs = {}; 253 | 254 | for (const metapakModuleName of metapakModulesSequence) { 255 | let base = ''; 256 | 257 | try { 258 | // Cover the case a metapak plugin runs itself 259 | if (metapakModuleName === packageConf.name) { 260 | base = PROJECT_DIR; 261 | } else { 262 | base = dirname( 263 | fileURLToPath(resolve(`${metapakModuleName}/package.json`)), 264 | ); 265 | } 266 | } catch (err) { 267 | throw YError.wrap( 268 | err as Error, 269 | 'E_MODULE_NOT_FOUND', 270 | metapakModuleName, 271 | packageConf.name, 272 | ); 273 | } 274 | const assetsDir = 'src'; 275 | const eventualBuildDir = join(base, 'dist'); 276 | let buildExists = false; 277 | 278 | try { 279 | await fs.accessAsync(eventualBuildDir); 280 | buildExists = true; 281 | } catch (err) { 282 | log('debug', `🏗 - No build path found (${eventualBuildDir}).`); 283 | log('debug-stack', printStackTrace(err as YError)); 284 | } 285 | 286 | const srcDir = buildExists ? 'dist' : 'src'; 287 | const fullSrcDir = join(base, srcDir); 288 | let configs: string[] = []; 289 | 290 | try { 291 | configs = await fs.readdirAsync(fullSrcDir); 292 | } catch (err) { 293 | log( 294 | 'error', 295 | `❌ - No configs found at "${fullSrcDir}" for the module "${metapakModuleName}".`, 296 | ); 297 | log('error-stack', printStackTrace(err as YError)); 298 | throw err; 299 | } 300 | 301 | moduleConfigs[metapakModuleName] = { 302 | base, 303 | assetsDir, 304 | srcDir, 305 | configs, 306 | }; 307 | log( 308 | 'debug', 309 | `📥 - Built config for "${metapakModuleName}:`, 310 | moduleConfigs[metapakModuleName], 311 | ); 312 | } 313 | 314 | return moduleConfigs; 315 | } 316 | -------------------------------------------------------------------------------- /src/services/packageConf.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, test, jest, expect } from '@jest/globals'; 2 | import { Knifecycle, constant } from 'knifecycle'; 3 | import initBuildPackageConf from './packageConf.js'; 4 | import type { BuildPackageConfService } from './packageConf.js'; 5 | import type { FSService } from './fs.js'; 6 | import type { ImporterService, LogService } from 'common-services'; 7 | import type { 8 | MetapakPackageJson, 9 | PackageJSONTransformer, 10 | } from '../libs/utils.js'; 11 | import type { JsonObject } from 'type-fest'; 12 | 13 | const METAPAK_SCRIPT = 'metapak'; 14 | 15 | describe('buildPackageConf', () => { 16 | const writeFileAsync = jest.fn(); 17 | const importer = jest.fn< 18 | ImporterService<{ 19 | default: PackageJSONTransformer; 20 | }> 21 | >(); 22 | const log = jest.fn(); 23 | let $: Knifecycle; 24 | 25 | beforeEach(() => { 26 | writeFileAsync.mockReset(); 27 | importer.mockReset(); 28 | log.mockReset(); 29 | 30 | $ = new Knifecycle(); 31 | $.register(constant('ENV', {})); 32 | $.register(constant('log', log)); 33 | $.register(constant('PROJECT_DIR', 'project/dir')); 34 | $.register( 35 | constant('fs', { 36 | writeFileAsync, 37 | }), 38 | ); 39 | $.register(constant('importer', importer)); 40 | $.register(initBuildPackageConf); 41 | }); 42 | 43 | test('should work with one module and one config', async () => { 44 | const packageConf: MetapakPackageJson = { 45 | metapak: { 46 | configs: ['_common'], 47 | data: {}, 48 | }, 49 | }; 50 | 51 | importer.mockResolvedValueOnce({ 52 | default: (packageConf) => { 53 | packageConf.private = true; 54 | return packageConf; 55 | }, 56 | }); 57 | writeFileAsync.mockResolvedValueOnce(undefined); 58 | 59 | const { buildPackageConf } = await $.run<{ 60 | buildPackageConf: BuildPackageConfService; 61 | }>(['buildPackageConf']); 62 | const result = await buildPackageConf(packageConf, { 63 | configsSequence: ['_common'], 64 | modulesSequence: ['metapak-http-server'], 65 | modulesConfigs: { 66 | 'metapak-http-server': { 67 | base: 'project/dir/node_modules/metapak-http-server', 68 | srcDir: 'src', 69 | assetsDir: 'src', 70 | configs: ['_common'], 71 | }, 72 | }, 73 | }); 74 | 75 | expect({ 76 | importerCalls: importer.mock.calls, 77 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 78 | logCalls: log.mock.calls.filter(filterLogs), 79 | result, 80 | }).toMatchInlineSnapshot(` 81 | { 82 | "importerCalls": [ 83 | [ 84 | "project/dir/node_modules/metapak-http-server/src/_common/package.js", 85 | ], 86 | ], 87 | "logCalls": [ 88 | [ 89 | "debug", 90 | "✅ - Package tranformation found at: project/dir/node_modules/metapak-http-server/src/_common/package.js", 91 | ], 92 | [ 93 | "debug", 94 | "💾 - Saving the package:", 95 | "project/dir/package.json", 96 | ], 97 | ], 98 | "result": true, 99 | "writeFileAsyncCalls": [ 100 | [ 101 | "project/dir/package.json", 102 | "{ 103 | "metapak": { 104 | "configs": [ 105 | "_common" 106 | ], 107 | "data": {} 108 | }, 109 | "scripts": { 110 | "metapak": "metapak" 111 | }, 112 | "private": true 113 | }", 114 | undefined, 115 | ], 116 | ], 117 | } 118 | `); 119 | }); 120 | 121 | test('should work with no tranformations', async () => { 122 | const packageConf: MetapakPackageJson = { 123 | metapak: { 124 | configs: ['_common'], 125 | data: {}, 126 | }, 127 | scripts: { 128 | metapak: METAPAK_SCRIPT, 129 | }, 130 | }; 131 | 132 | importer.mockRejectedValue(new Error('E_ERROR')); 133 | writeFileAsync.mockResolvedValue(undefined); 134 | 135 | const { buildPackageConf } = await $.run<{ 136 | buildPackageConf: BuildPackageConfService; 137 | }>(['buildPackageConf']); 138 | const result = await buildPackageConf(packageConf, { 139 | configsSequence: ['_common'], 140 | modulesSequence: ['metapak-http-server'], 141 | modulesConfigs: { 142 | 'metapak-http-server': { 143 | base: 'project/dir/node_modules/metapak-http-server', 144 | srcDir: 'src', 145 | assetsDir: 'src', 146 | configs: ['_common'], 147 | }, 148 | }, 149 | }); 150 | 151 | expect({ 152 | importerCalls: importer.mock.calls, 153 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 154 | logCalls: log.mock.calls.filter(filterLogs), 155 | result, 156 | }).toMatchInlineSnapshot(` 157 | { 158 | "importerCalls": [ 159 | [ 160 | "project/dir/node_modules/metapak-http-server/src/_common/package.js", 161 | ], 162 | ], 163 | "logCalls": [ 164 | [ 165 | "debug", 166 | "🤷 - No package tranformation found at: project/dir/node_modules/metapak-http-server/src/_common/package.js", 167 | ], 168 | ], 169 | "result": false, 170 | "writeFileAsyncCalls": [], 171 | } 172 | `); 173 | }); 174 | 175 | test('should work with several modules and configs', async () => { 176 | const packageConf: MetapakPackageJson = { 177 | metapak: { 178 | configs: ['_common', 'author'], 179 | data: {}, 180 | }, 181 | scripts: { 182 | metapak: METAPAK_SCRIPT, 183 | }, 184 | }; 185 | 186 | importer.mockResolvedValueOnce({ 187 | default: (packageConf) => { 188 | packageConf.private = true; 189 | return packageConf; 190 | }, 191 | }); 192 | importer.mockResolvedValueOnce({ 193 | default: (packageConf) => { 194 | packageConf.license = 'MIT'; 195 | return packageConf; 196 | }, 197 | }); 198 | importer.mockResolvedValueOnce({ 199 | default: (packageConf) => { 200 | packageConf.author = 'John Doe'; 201 | return packageConf; 202 | }, 203 | }); 204 | writeFileAsync.mockResolvedValue(undefined); 205 | 206 | const { buildPackageConf } = await $.run<{ 207 | buildPackageConf: BuildPackageConfService; 208 | }>(['buildPackageConf']); 209 | const result = await buildPackageConf(packageConf, { 210 | configsSequence: ['_common', 'author'], 211 | modulesSequence: ['metapak-http-server', 'metapak-schmilbik'], 212 | modulesConfigs: { 213 | 'metapak-http-server': { 214 | base: 'project/dir/node_modules/metapak-http-server', 215 | srcDir: 'src', 216 | assetsDir: 'src', 217 | configs: ['_common'], 218 | }, 219 | 'metapak-schmilbik': { 220 | base: 'project/dir/node_modules/metapak-schmilbik', 221 | srcDir: 'src', 222 | assetsDir: 'src', 223 | configs: ['_common', 'author'], 224 | }, 225 | }, 226 | }); 227 | 228 | expect({ 229 | importerCalls: importer.mock.calls, 230 | writeFileAsyncCalls: writeFileAsync.mock.calls.map(bufferToText), 231 | logCalls: log.mock.calls.filter(filterLogs), 232 | result, 233 | }).toMatchInlineSnapshot(` 234 | { 235 | "importerCalls": [ 236 | [ 237 | "project/dir/node_modules/metapak-http-server/src/_common/package.js", 238 | ], 239 | [ 240 | "project/dir/node_modules/metapak-schmilbik/src/_common/package.js", 241 | ], 242 | [ 243 | "project/dir/node_modules/metapak-http-server/src/author/package.js", 244 | ], 245 | [ 246 | "project/dir/node_modules/metapak-schmilbik/src/author/package.js", 247 | ], 248 | ], 249 | "logCalls": [ 250 | [ 251 | "debug", 252 | "✅ - Package tranformation found at: project/dir/node_modules/metapak-http-server/src/_common/package.js", 253 | ], 254 | [ 255 | "debug", 256 | "✅ - Package tranformation found at: project/dir/node_modules/metapak-schmilbik/src/_common/package.js", 257 | ], 258 | [ 259 | "debug", 260 | "✅ - Package tranformation found at: project/dir/node_modules/metapak-http-server/src/author/package.js", 261 | ], 262 | [ 263 | "debug", 264 | "🤷 - No package tranformation found at: project/dir/node_modules/metapak-schmilbik/src/author/package.js", 265 | ], 266 | [ 267 | "debug", 268 | "💾 - Saving the package:", 269 | "project/dir/package.json", 270 | ], 271 | ], 272 | "result": true, 273 | "writeFileAsyncCalls": [ 274 | [ 275 | "project/dir/package.json", 276 | "{ 277 | "metapak": { 278 | "configs": [ 279 | "_common", 280 | "author" 281 | ], 282 | "data": {} 283 | }, 284 | "scripts": { 285 | "metapak": "metapak" 286 | }, 287 | "private": true, 288 | "license": "MIT", 289 | "author": "John Doe" 290 | }", 291 | undefined, 292 | ], 293 | ], 294 | } 295 | `); 296 | }); 297 | }); 298 | 299 | function bufferToText( 300 | call: Parameters, 301 | ): [string, string, Parameters[2]] { 302 | return [call[0], call[1].toString(), call[2]]; 303 | } 304 | 305 | function filterLogs(e: Parameters) { 306 | return !e[0].endsWith('-stack'); 307 | } 308 | -------------------------------------------------------------------------------- /src/services/packageConf.ts: -------------------------------------------------------------------------------- 1 | import { autoService } from 'knifecycle'; 2 | import sortKeys from 'sort-keys'; 3 | import { isDeepStrictEqual } from 'util'; 4 | import path from 'path'; 5 | import { mapConfigsSequentially, identity, buildDiff } from '../libs/utils.js'; 6 | import { YError, printStackTrace } from 'yerror'; 7 | import type { MetapakContext } from '../libs/utils.js'; 8 | import type { ImporterService, LogService } from 'common-services'; 9 | import type { 10 | PackageJSONTransformer, 11 | MetapakPackageJson, 12 | } from '../libs/utils.js'; 13 | import type { FSService } from './fs.js'; 14 | 15 | export type BuildPackageConfService = ( 16 | packageConf: MetapakPackageJson, 17 | metapakContext: MetapakContext, 18 | ) => Promise; 19 | 20 | const METAPAK_SCRIPT = 'metapak'; 21 | 22 | export default autoService(initBuildPackageConf); 23 | 24 | async function initBuildPackageConf({ 25 | PROJECT_DIR, 26 | fs, 27 | importer, 28 | log, 29 | }: { 30 | PROJECT_DIR: string; 31 | fs: Pick; 32 | importer: ImporterService<{ 33 | default: PackageJSONTransformer; 34 | }>; 35 | log: LogService; 36 | }): Promise { 37 | return async ( 38 | packageConf: MetapakPackageJson, 39 | metapakContext: MetapakContext, 40 | ) => { 41 | const originalDependencies = Object.keys(packageConf.dependencies || {}); 42 | const originalPackageConf = JSON.stringify(packageConf, null, 2); 43 | 44 | const packageTransformers = await mapConfigsSequentially( 45 | metapakContext, 46 | async ( 47 | metapakModuleName: string, 48 | metapakConfigName: string, 49 | ): Promise> => { 50 | const packageTransformPath = path.join( 51 | metapakContext.modulesConfigs[metapakModuleName].base, 52 | metapakContext.modulesConfigs[metapakModuleName].srcDir, 53 | metapakConfigName, 54 | 'package.js', 55 | ); 56 | 57 | try { 58 | const transformer = (await importer(packageTransformPath)).default; 59 | 60 | log( 61 | 'debug', 62 | `✅ - Package tranformation found at: ${packageTransformPath}`, 63 | ); 64 | 65 | return transformer; 66 | } catch (err) { 67 | log( 68 | 'debug', 69 | `🤷 - No package tranformation found at: ${packageTransformPath}`, 70 | ); 71 | log('debug-stack', printStackTrace(err as YError)); 72 | } 73 | return identity; 74 | }, 75 | ); 76 | 77 | let newPackageConf: MetapakPackageJson = packageConf; 78 | 79 | // Adding the `metapak` postinstall script via an idempotent way 80 | newPackageConf.scripts = packageConf.scripts || {}; 81 | if ('metapak' !== packageConf.name) { 82 | newPackageConf.scripts.metapak = METAPAK_SCRIPT; 83 | } 84 | newPackageConf = packageTransformers.reduce( 85 | (newPackageConf, packageTransformer) => 86 | packageTransformer(newPackageConf), 87 | packageConf, 88 | ); 89 | if ( 90 | Object.keys(newPackageConf.dependencies || {}) 91 | .sort() 92 | .join() !== originalDependencies.sort().join() 93 | ) { 94 | log( 95 | 'warning', 96 | '⚠️ - Changing dependencies with metapak is not recommended!', 97 | ); 98 | } 99 | if (newPackageConf.dependencies) { 100 | newPackageConf.dependencies = sortKeys(newPackageConf.dependencies); 101 | } 102 | if (newPackageConf.devDependencies) { 103 | newPackageConf.devDependencies = sortKeys(newPackageConf.devDependencies); 104 | } 105 | if (newPackageConf.scripts) { 106 | newPackageConf.scripts = sortKeys(newPackageConf.scripts); 107 | } 108 | 109 | const data = JSON.stringify(newPackageConf, null, 2); 110 | 111 | if ( 112 | originalPackageConf === data || 113 | isDeepStrictEqual(JSON.parse(originalPackageConf), JSON.parse(data)) 114 | ) { 115 | return false; 116 | } 117 | 118 | log('debug-stack', buildDiff(originalPackageConf, data)); 119 | 120 | log( 121 | 'debug', 122 | '💾 - Saving the package:', 123 | path.join(PROJECT_DIR, 'package.json'), 124 | ); 125 | 126 | await fs.writeFileAsync( 127 | path.join(PROJECT_DIR, 'package.json'), 128 | Buffer.from(data, 'utf-8'), 129 | ); 130 | 131 | return true; 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /src/services/programOptions.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { program } from 'commander'; 4 | import { autoService } from 'knifecycle'; 5 | 6 | export type ProgramOptionsService = { 7 | safe?: boolean; 8 | dryRun?: boolean; 9 | base: string; 10 | }; 11 | 12 | async function initProgramOptions(): Promise { 13 | return program 14 | .version( 15 | JSON.parse(fs.readFileSync(path.join('.', 'package.json')).toString()), 16 | ) 17 | .option('-s, --safe', 'Exit with 1 when changes are detected') 18 | .option('-d, --dry-run', 'Print the changes without doing it') 19 | .option('-b, --base [value]', 'Base for links') 20 | .parse(process.argv) 21 | .opts(); 22 | } 23 | 24 | export default autoService(initProgramOptions); 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "moduleResolution": "Node16", 5 | "target": "es2022", 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "preserveConstEnums": true, 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "declaration": true, 13 | "outDir": "dist", 14 | "sourceMap": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } --------------------------------------------------------------------------------