├── .gitignore ├── .vscode └── settings.json ├── doc ├── css.png ├── emission.py ├── font.nfo ├── font.ttf ├── hammer.blend ├── hammer.blend1 ├── hammer.png ├── html.png ├── javascript.png ├── logo.png ├── node.png └── typescript.png ├── hammer.mjs ├── license ├── package-lock.json ├── package.json ├── readme.md ├── src ├── async │ ├── channel.ts │ ├── debounce.ts │ ├── index.ts │ └── into.ts ├── build │ ├── build.ts │ └── index.ts ├── cache │ ├── cache.ts │ └── index.ts ├── cli.ts ├── dispose.ts ├── evaluate │ ├── compile │ │ ├── compile.ts │ │ └── index.ts │ ├── evaluate.ts │ ├── execute │ │ ├── execute.ts │ │ └── index.ts │ └── index.ts ├── hammer ├── index.ts ├── monitor │ ├── index.ts │ └── monitor.ts ├── options │ ├── index.ts │ └── options.ts ├── resolve │ ├── index.ts │ └── resolve.ts ├── run │ ├── index.ts │ └── run.ts ├── serve │ ├── index.ts │ ├── mime.ts │ ├── server.ts │ └── static.ts ├── shell │ ├── index.ts │ └── shell.ts ├── task │ ├── global │ │ ├── delay.ts │ │ ├── error.ts │ │ ├── file.ts │ │ ├── folder.ts │ │ ├── fs.ts │ │ ├── index.ts │ │ ├── shell.ts │ │ ├── supports │ │ │ └── supports.ts │ │ └── watch.ts │ ├── index.ts │ └── task.ts ├── tsconfig.json └── watch │ ├── index.ts │ └── watch.ts ├── tasks.js └── website ├── about.html ├── contact.html ├── images └── hammer.png ├── index.html ├── scripts ├── index.ts └── test.ts ├── server.ts └── styles └── index.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | target -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "node_modules": true, 4 | "package-lock.json": true 5 | } 6 | } -------------------------------------------------------------------------------- /doc/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/css.png -------------------------------------------------------------------------------- /doc/emission.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from random import randint 3 | 4 | cube = bpy.data.objects["Cube"] 5 | material = cube.active_material 6 | # material = bpy.data.materials['Material'] 7 | 8 | frame_number = 0 9 | 10 | for i in range(0, 5): 11 | bpy.context.scene.frame_set(frame_number) 12 | 13 | cube.location = (0, 0, 0) 14 | cube.keyframe_insert(data_path="location", frame=frame_number) 15 | 16 | strength = ((1 / 4) * i) * 1 17 | material.node_tree.nodes['Emission'].inputs['Strength'].default_value = strength 18 | material.node_tree.nodes['Emission'].inputs['Strength'].keyframe_insert(data_path='default_value', frame=frame_number) 19 | frame_number += 24 20 | 21 | print ('done') -------------------------------------------------------------------------------- /doc/font.nfo: -------------------------------------------------------------------------------- 1 | Conthrax -------------------------------------------------------------------------------- /doc/font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/font.ttf -------------------------------------------------------------------------------- /doc/hammer.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/hammer.blend -------------------------------------------------------------------------------- /doc/hammer.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/hammer.blend1 -------------------------------------------------------------------------------- /doc/hammer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/hammer.png -------------------------------------------------------------------------------- /doc/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/html.png -------------------------------------------------------------------------------- /doc/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/javascript.png -------------------------------------------------------------------------------- /doc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/logo.png -------------------------------------------------------------------------------- /doc/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/node.png -------------------------------------------------------------------------------- /doc/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/doc/typescript.png -------------------------------------------------------------------------------- /hammer.mjs: -------------------------------------------------------------------------------- 1 | export async function clean() { 2 | console.log('hammer: clean') 3 | } 4 | 5 | export async function start() { 6 | console.log('hammer: start') 7 | } 8 | 9 | export async function build() { 10 | console.log('hammer: build') 11 | } 12 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sinclair/hammer", 3 | "version": "0.18.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@sinclair/hammer", 9 | "version": "0.18.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "esbuild": "0.15.7" 13 | }, 14 | "bin": { 15 | "hammer": "hammer" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^14.14.41", 19 | "prettier": "^2.7.1", 20 | "smoke-run": "^1.4.0", 21 | "smoke-task": "^1.1.4", 22 | "typescript": "^4.2.4" 23 | } 24 | }, 25 | "node_modules/@esbuild/linux-loong64": { 26 | "version": "0.15.7", 27 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", 28 | "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", 29 | "cpu": [ 30 | "loong64" 31 | ], 32 | "optional": true, 33 | "os": [ 34 | "linux" 35 | ], 36 | "engines": { 37 | "node": ">=12" 38 | } 39 | }, 40 | "node_modules/@types/node": { 41 | "version": "14.18.27", 42 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.27.tgz", 43 | "integrity": "sha512-DcTUcwT9xEcf4rp2UHyGAcmlqG4Mhe7acozl5vY2xzSrwP1z19ZVyjzQ6DsNUrvIadpiyZoQCTHFt4t2omYIZQ==", 44 | "dev": true 45 | }, 46 | "node_modules/esbuild": { 47 | "version": "0.15.7", 48 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", 49 | "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", 50 | "hasInstallScript": true, 51 | "bin": { 52 | "esbuild": "bin/esbuild" 53 | }, 54 | "engines": { 55 | "node": ">=12" 56 | }, 57 | "optionalDependencies": { 58 | "@esbuild/linux-loong64": "0.15.7", 59 | "esbuild-android-64": "0.15.7", 60 | "esbuild-android-arm64": "0.15.7", 61 | "esbuild-darwin-64": "0.15.7", 62 | "esbuild-darwin-arm64": "0.15.7", 63 | "esbuild-freebsd-64": "0.15.7", 64 | "esbuild-freebsd-arm64": "0.15.7", 65 | "esbuild-linux-32": "0.15.7", 66 | "esbuild-linux-64": "0.15.7", 67 | "esbuild-linux-arm": "0.15.7", 68 | "esbuild-linux-arm64": "0.15.7", 69 | "esbuild-linux-mips64le": "0.15.7", 70 | "esbuild-linux-ppc64le": "0.15.7", 71 | "esbuild-linux-riscv64": "0.15.7", 72 | "esbuild-linux-s390x": "0.15.7", 73 | "esbuild-netbsd-64": "0.15.7", 74 | "esbuild-openbsd-64": "0.15.7", 75 | "esbuild-sunos-64": "0.15.7", 76 | "esbuild-windows-32": "0.15.7", 77 | "esbuild-windows-64": "0.15.7", 78 | "esbuild-windows-arm64": "0.15.7" 79 | } 80 | }, 81 | "node_modules/esbuild-android-64": { 82 | "version": "0.15.7", 83 | "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", 84 | "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", 85 | "cpu": [ 86 | "x64" 87 | ], 88 | "optional": true, 89 | "os": [ 90 | "android" 91 | ], 92 | "engines": { 93 | "node": ">=12" 94 | } 95 | }, 96 | "node_modules/esbuild-android-arm64": { 97 | "version": "0.15.7", 98 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", 99 | "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", 100 | "cpu": [ 101 | "arm64" 102 | ], 103 | "optional": true, 104 | "os": [ 105 | "android" 106 | ], 107 | "engines": { 108 | "node": ">=12" 109 | } 110 | }, 111 | "node_modules/esbuild-darwin-64": { 112 | "version": "0.15.7", 113 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", 114 | "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", 115 | "cpu": [ 116 | "x64" 117 | ], 118 | "optional": true, 119 | "os": [ 120 | "darwin" 121 | ], 122 | "engines": { 123 | "node": ">=12" 124 | } 125 | }, 126 | "node_modules/esbuild-darwin-arm64": { 127 | "version": "0.15.7", 128 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", 129 | "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", 130 | "cpu": [ 131 | "arm64" 132 | ], 133 | "optional": true, 134 | "os": [ 135 | "darwin" 136 | ], 137 | "engines": { 138 | "node": ">=12" 139 | } 140 | }, 141 | "node_modules/esbuild-freebsd-64": { 142 | "version": "0.15.7", 143 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", 144 | "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", 145 | "cpu": [ 146 | "x64" 147 | ], 148 | "optional": true, 149 | "os": [ 150 | "freebsd" 151 | ], 152 | "engines": { 153 | "node": ">=12" 154 | } 155 | }, 156 | "node_modules/esbuild-freebsd-arm64": { 157 | "version": "0.15.7", 158 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", 159 | "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", 160 | "cpu": [ 161 | "arm64" 162 | ], 163 | "optional": true, 164 | "os": [ 165 | "freebsd" 166 | ], 167 | "engines": { 168 | "node": ">=12" 169 | } 170 | }, 171 | "node_modules/esbuild-linux-32": { 172 | "version": "0.15.7", 173 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", 174 | "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", 175 | "cpu": [ 176 | "ia32" 177 | ], 178 | "optional": true, 179 | "os": [ 180 | "linux" 181 | ], 182 | "engines": { 183 | "node": ">=12" 184 | } 185 | }, 186 | "node_modules/esbuild-linux-64": { 187 | "version": "0.15.7", 188 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", 189 | "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", 190 | "cpu": [ 191 | "x64" 192 | ], 193 | "optional": true, 194 | "os": [ 195 | "linux" 196 | ], 197 | "engines": { 198 | "node": ">=12" 199 | } 200 | }, 201 | "node_modules/esbuild-linux-arm": { 202 | "version": "0.15.7", 203 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", 204 | "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", 205 | "cpu": [ 206 | "arm" 207 | ], 208 | "optional": true, 209 | "os": [ 210 | "linux" 211 | ], 212 | "engines": { 213 | "node": ">=12" 214 | } 215 | }, 216 | "node_modules/esbuild-linux-arm64": { 217 | "version": "0.15.7", 218 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", 219 | "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", 220 | "cpu": [ 221 | "arm64" 222 | ], 223 | "optional": true, 224 | "os": [ 225 | "linux" 226 | ], 227 | "engines": { 228 | "node": ">=12" 229 | } 230 | }, 231 | "node_modules/esbuild-linux-mips64le": { 232 | "version": "0.15.7", 233 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", 234 | "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", 235 | "cpu": [ 236 | "mips64el" 237 | ], 238 | "optional": true, 239 | "os": [ 240 | "linux" 241 | ], 242 | "engines": { 243 | "node": ">=12" 244 | } 245 | }, 246 | "node_modules/esbuild-linux-ppc64le": { 247 | "version": "0.15.7", 248 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", 249 | "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", 250 | "cpu": [ 251 | "ppc64" 252 | ], 253 | "optional": true, 254 | "os": [ 255 | "linux" 256 | ], 257 | "engines": { 258 | "node": ">=12" 259 | } 260 | }, 261 | "node_modules/esbuild-linux-riscv64": { 262 | "version": "0.15.7", 263 | "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", 264 | "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", 265 | "cpu": [ 266 | "riscv64" 267 | ], 268 | "optional": true, 269 | "os": [ 270 | "linux" 271 | ], 272 | "engines": { 273 | "node": ">=12" 274 | } 275 | }, 276 | "node_modules/esbuild-linux-s390x": { 277 | "version": "0.15.7", 278 | "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", 279 | "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", 280 | "cpu": [ 281 | "s390x" 282 | ], 283 | "optional": true, 284 | "os": [ 285 | "linux" 286 | ], 287 | "engines": { 288 | "node": ">=12" 289 | } 290 | }, 291 | "node_modules/esbuild-netbsd-64": { 292 | "version": "0.15.7", 293 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", 294 | "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", 295 | "cpu": [ 296 | "x64" 297 | ], 298 | "optional": true, 299 | "os": [ 300 | "netbsd" 301 | ], 302 | "engines": { 303 | "node": ">=12" 304 | } 305 | }, 306 | "node_modules/esbuild-openbsd-64": { 307 | "version": "0.15.7", 308 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", 309 | "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", 310 | "cpu": [ 311 | "x64" 312 | ], 313 | "optional": true, 314 | "os": [ 315 | "openbsd" 316 | ], 317 | "engines": { 318 | "node": ">=12" 319 | } 320 | }, 321 | "node_modules/esbuild-sunos-64": { 322 | "version": "0.15.7", 323 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", 324 | "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", 325 | "cpu": [ 326 | "x64" 327 | ], 328 | "optional": true, 329 | "os": [ 330 | "sunos" 331 | ], 332 | "engines": { 333 | "node": ">=12" 334 | } 335 | }, 336 | "node_modules/esbuild-windows-32": { 337 | "version": "0.15.7", 338 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", 339 | "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", 340 | "cpu": [ 341 | "ia32" 342 | ], 343 | "optional": true, 344 | "os": [ 345 | "win32" 346 | ], 347 | "engines": { 348 | "node": ">=12" 349 | } 350 | }, 351 | "node_modules/esbuild-windows-64": { 352 | "version": "0.15.7", 353 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", 354 | "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", 355 | "cpu": [ 356 | "x64" 357 | ], 358 | "optional": true, 359 | "os": [ 360 | "win32" 361 | ], 362 | "engines": { 363 | "node": ">=12" 364 | } 365 | }, 366 | "node_modules/esbuild-windows-arm64": { 367 | "version": "0.15.7", 368 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", 369 | "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", 370 | "cpu": [ 371 | "arm64" 372 | ], 373 | "optional": true, 374 | "os": [ 375 | "win32" 376 | ], 377 | "engines": { 378 | "node": ">=12" 379 | } 380 | }, 381 | "node_modules/prettier": { 382 | "version": "2.7.1", 383 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", 384 | "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", 385 | "dev": true, 386 | "bin": { 387 | "prettier": "bin-prettier.js" 388 | }, 389 | "engines": { 390 | "node": ">=10.13.0" 391 | }, 392 | "funding": { 393 | "url": "https://github.com/prettier/prettier?sponsor=1" 394 | } 395 | }, 396 | "node_modules/smoke-run": { 397 | "version": "1.4.0", 398 | "resolved": "https://registry.npmjs.org/smoke-run/-/smoke-run-1.4.0.tgz", 399 | "integrity": "sha512-yPHCkKmaqQ4JPrFQSojTElK/bmtnPgoV89wodW8hKVMz93ZDHgN4X8k7R6Yeqt1/P5nD9ophszxeTjCSGze2pg==", 400 | "dev": true, 401 | "bin": { 402 | "smoke-run": "smoke-run" 403 | } 404 | }, 405 | "node_modules/smoke-task": { 406 | "version": "1.1.4", 407 | "resolved": "https://registry.npmjs.org/smoke-task/-/smoke-task-1.1.4.tgz", 408 | "integrity": "sha512-BbgBKIbwg19bNt847PQgFJUgxPniPv6ETZ6u+yF8QSPfO+lfjNKpmjrrJgaUgbLDHT3iZ589kRqwg5rieTTTlA==", 409 | "dev": true, 410 | "bin": { 411 | "smoke-task": "start.js" 412 | } 413 | }, 414 | "node_modules/typescript": { 415 | "version": "4.8.2", 416 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", 417 | "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", 418 | "dev": true, 419 | "bin": { 420 | "tsc": "bin/tsc", 421 | "tsserver": "bin/tsserver" 422 | }, 423 | "engines": { 424 | "node": ">=4.2.0" 425 | } 426 | } 427 | }, 428 | "dependencies": { 429 | "@esbuild/linux-loong64": { 430 | "version": "0.15.7", 431 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.7.tgz", 432 | "integrity": "sha512-IKznSJOsVUuyt7cDzzSZyqBEcZe+7WlBqTVXiF1OXP/4Nm387ToaXZ0fyLwI1iBlI/bzpxVq411QE2/Bt2XWWw==", 433 | "optional": true 434 | }, 435 | "@types/node": { 436 | "version": "14.18.27", 437 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.27.tgz", 438 | "integrity": "sha512-DcTUcwT9xEcf4rp2UHyGAcmlqG4Mhe7acozl5vY2xzSrwP1z19ZVyjzQ6DsNUrvIadpiyZoQCTHFt4t2omYIZQ==", 439 | "dev": true 440 | }, 441 | "esbuild": { 442 | "version": "0.15.7", 443 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.7.tgz", 444 | "integrity": "sha512-7V8tzllIbAQV1M4QoE52ImKu8hT/NLGlGXkiDsbEU5PS6K8Mn09ZnYoS+dcmHxOS9CRsV4IRAMdT3I67IyUNXw==", 445 | "requires": { 446 | "@esbuild/linux-loong64": "0.15.7", 447 | "esbuild-android-64": "0.15.7", 448 | "esbuild-android-arm64": "0.15.7", 449 | "esbuild-darwin-64": "0.15.7", 450 | "esbuild-darwin-arm64": "0.15.7", 451 | "esbuild-freebsd-64": "0.15.7", 452 | "esbuild-freebsd-arm64": "0.15.7", 453 | "esbuild-linux-32": "0.15.7", 454 | "esbuild-linux-64": "0.15.7", 455 | "esbuild-linux-arm": "0.15.7", 456 | "esbuild-linux-arm64": "0.15.7", 457 | "esbuild-linux-mips64le": "0.15.7", 458 | "esbuild-linux-ppc64le": "0.15.7", 459 | "esbuild-linux-riscv64": "0.15.7", 460 | "esbuild-linux-s390x": "0.15.7", 461 | "esbuild-netbsd-64": "0.15.7", 462 | "esbuild-openbsd-64": "0.15.7", 463 | "esbuild-sunos-64": "0.15.7", 464 | "esbuild-windows-32": "0.15.7", 465 | "esbuild-windows-64": "0.15.7", 466 | "esbuild-windows-arm64": "0.15.7" 467 | } 468 | }, 469 | "esbuild-android-64": { 470 | "version": "0.15.7", 471 | "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.7.tgz", 472 | "integrity": "sha512-p7rCvdsldhxQr3YHxptf1Jcd86dlhvc3EQmQJaZzzuAxefO9PvcI0GLOa5nCWem1AJ8iMRu9w0r5TG8pHmbi9w==", 473 | "optional": true 474 | }, 475 | "esbuild-android-arm64": { 476 | "version": "0.15.7", 477 | "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.7.tgz", 478 | "integrity": "sha512-L775l9ynJT7rVqRM5vo+9w5g2ysbOCfsdLV4CWanTZ1k/9Jb3IYlQ06VCI1edhcosTYJRECQFJa3eAvkx72eyQ==", 479 | "optional": true 480 | }, 481 | "esbuild-darwin-64": { 482 | "version": "0.15.7", 483 | "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.7.tgz", 484 | "integrity": "sha512-KGPt3r1c9ww009t2xLB6Vk0YyNOXh7hbjZ3EecHoVDxgtbUlYstMPDaReimKe6eOEfyY4hBEEeTvKwPsiH5WZg==", 485 | "optional": true 486 | }, 487 | "esbuild-darwin-arm64": { 488 | "version": "0.15.7", 489 | "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.7.tgz", 490 | "integrity": "sha512-kBIHvtVqbSGajN88lYMnR3aIleH3ABZLLFLxwL2stiuIGAjGlQW741NxVTpUHQXUmPzxi6POqc9npkXa8AcSZQ==", 491 | "optional": true 492 | }, 493 | "esbuild-freebsd-64": { 494 | "version": "0.15.7", 495 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.7.tgz", 496 | "integrity": "sha512-hESZB91qDLV5MEwNxzMxPfbjAhOmtfsr9Wnuci7pY6TtEh4UDuevmGmkUIjX/b+e/k4tcNBMf7SRQ2mdNuK/HQ==", 497 | "optional": true 498 | }, 499 | "esbuild-freebsd-arm64": { 500 | "version": "0.15.7", 501 | "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.7.tgz", 502 | "integrity": "sha512-dLFR0ChH5t+b3J8w0fVKGvtwSLWCv7GYT2Y2jFGulF1L5HftQLzVGN+6pi1SivuiVSmTh28FwUhi9PwQicXI6Q==", 503 | "optional": true 504 | }, 505 | "esbuild-linux-32": { 506 | "version": "0.15.7", 507 | "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.7.tgz", 508 | "integrity": "sha512-v3gT/LsONGUZcjbt2swrMjwxo32NJzk+7sAgtxhGx1+ZmOFaTRXBAi1PPfgpeo/J//Un2jIKm/I+qqeo4caJvg==", 509 | "optional": true 510 | }, 511 | "esbuild-linux-64": { 512 | "version": "0.15.7", 513 | "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.7.tgz", 514 | "integrity": "sha512-LxXEfLAKwOVmm1yecpMmWERBshl+Kv5YJ/1KnyAr6HRHFW8cxOEsEfisD3sVl/RvHyW//lhYUVSuy9jGEfIRAQ==", 515 | "optional": true 516 | }, 517 | "esbuild-linux-arm": { 518 | "version": "0.15.7", 519 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.7.tgz", 520 | "integrity": "sha512-JKgAHtMR5f75wJTeuNQbyznZZa+pjiUHV7sRZp42UNdyXC6TiUYMW/8z8yIBAr2Fpad8hM1royZKQisqPABPvQ==", 521 | "optional": true 522 | }, 523 | "esbuild-linux-arm64": { 524 | "version": "0.15.7", 525 | "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.7.tgz", 526 | "integrity": "sha512-P3cfhudpzWDkglutWgXcT2S7Ft7o2e3YDMrP1n0z2dlbUZghUkKCyaWw0zhp4KxEEzt/E7lmrtRu/pGWnwb9vw==", 527 | "optional": true 528 | }, 529 | "esbuild-linux-mips64le": { 530 | "version": "0.15.7", 531 | "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.7.tgz", 532 | "integrity": "sha512-T7XKuxl0VpeFLCJXub6U+iybiqh0kM/bWOTb4qcPyDDwNVhLUiPcGdG2/0S7F93czUZOKP57YiLV8YQewgLHKw==", 533 | "optional": true 534 | }, 535 | "esbuild-linux-ppc64le": { 536 | "version": "0.15.7", 537 | "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.7.tgz", 538 | "integrity": "sha512-6mGuC19WpFN7NYbecMIJjeQgvDb5aMuvyk0PDYBJrqAEMkTwg3Z98kEKuCm6THHRnrgsdr7bp4SruSAxEM4eJw==", 539 | "optional": true 540 | }, 541 | "esbuild-linux-riscv64": { 542 | "version": "0.15.7", 543 | "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.7.tgz", 544 | "integrity": "sha512-uUJsezbswAYo/X7OU/P+PuL/EI9WzxsEQXDekfwpQ23uGiooxqoLFAPmXPcRAt941vjlY9jtITEEikWMBr+F/g==", 545 | "optional": true 546 | }, 547 | "esbuild-linux-s390x": { 548 | "version": "0.15.7", 549 | "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.7.tgz", 550 | "integrity": "sha512-+tO+xOyTNMc34rXlSxK7aCwJgvQyffqEM5MMdNDEeMU3ss0S6wKvbBOQfgd5jRPblfwJ6b+bKiz0g5nABpY0QQ==", 551 | "optional": true 552 | }, 553 | "esbuild-netbsd-64": { 554 | "version": "0.15.7", 555 | "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.7.tgz", 556 | "integrity": "sha512-yVc4Wz+Pu3cP5hzm5kIygNPrjar/v5WCSoRmIjCPWfBVJkZNb5brEGKUlf+0Y759D48BCWa0WHrWXaNy0DULTQ==", 557 | "optional": true 558 | }, 559 | "esbuild-openbsd-64": { 560 | "version": "0.15.7", 561 | "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.7.tgz", 562 | "integrity": "sha512-GsimbwC4FSR4lN3wf8XmTQ+r8/0YSQo21rWDL0XFFhLHKlzEA4SsT1Tl8bPYu00IU6UWSJ+b3fG/8SB69rcuEQ==", 563 | "optional": true 564 | }, 565 | "esbuild-sunos-64": { 566 | "version": "0.15.7", 567 | "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.7.tgz", 568 | "integrity": "sha512-8CDI1aL/ts0mDGbWzjEOGKXnU7p3rDzggHSBtVryQzkSOsjCHRVe0iFYUuhczlxU1R3LN/E7HgUO4NXzGGP/Ag==", 569 | "optional": true 570 | }, 571 | "esbuild-windows-32": { 572 | "version": "0.15.7", 573 | "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.7.tgz", 574 | "integrity": "sha512-cOnKXUEPS8EGCzRSFa1x6NQjGhGsFlVgjhqGEbLTPsA7x4RRYiy2RKoArNUU4iR2vHmzqS5Gr84MEumO/wxYKA==", 575 | "optional": true 576 | }, 577 | "esbuild-windows-64": { 578 | "version": "0.15.7", 579 | "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.7.tgz", 580 | "integrity": "sha512-7MI08Ec2sTIDv+zH6StNBKO+2hGUYIT42GmFyW6MBBWWtJhTcQLinKS6ldIN1d52MXIbiJ6nXyCJ+LpL4jBm3Q==", 581 | "optional": true 582 | }, 583 | "esbuild-windows-arm64": { 584 | "version": "0.15.7", 585 | "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.7.tgz", 586 | "integrity": "sha512-R06nmqBlWjKHddhRJYlqDd3Fabx9LFdKcjoOy08YLimwmsswlFBJV4rXzZCxz/b7ZJXvrZgj8DDv1ewE9+StMw==", 587 | "optional": true 588 | }, 589 | "prettier": { 590 | "version": "2.7.1", 591 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", 592 | "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", 593 | "dev": true 594 | }, 595 | "smoke-run": { 596 | "version": "1.4.0", 597 | "resolved": "https://registry.npmjs.org/smoke-run/-/smoke-run-1.4.0.tgz", 598 | "integrity": "sha512-yPHCkKmaqQ4JPrFQSojTElK/bmtnPgoV89wodW8hKVMz93ZDHgN4X8k7R6Yeqt1/P5nD9ophszxeTjCSGze2pg==", 599 | "dev": true 600 | }, 601 | "smoke-task": { 602 | "version": "1.1.4", 603 | "resolved": "https://registry.npmjs.org/smoke-task/-/smoke-task-1.1.4.tgz", 604 | "integrity": "sha512-BbgBKIbwg19bNt847PQgFJUgxPniPv6ETZ6u+yF8QSPfO+lfjNKpmjrrJgaUgbLDHT3iZ589kRqwg5rieTTTlA==", 605 | "dev": true 606 | }, 607 | "typescript": { 608 | "version": "4.8.2", 609 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz", 610 | "integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==", 611 | "dev": true 612 | } 613 | } 614 | } 615 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sinclair/hammer", 3 | "version": "0.18.0", 4 | "description": "Build Tool for Browser and Node Applications", 5 | "author": "sinclairzx81", 6 | "keywords": [ 7 | "esbuild", 8 | "bundle", 9 | "html", 10 | "node", 11 | "watch", 12 | "tasks", 13 | "esm" 14 | ], 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/sinclairzx81/hammer" 19 | }, 20 | "main": "index.js", 21 | "bin": { 22 | "hammer": "./hammer" 23 | }, 24 | "scripts": { 25 | "clean": "smoke-task clean", 26 | "format": "smoke-task format", 27 | "build": "smoke-task build", 28 | "start": "smoke-task start", 29 | "install_cli": "smoke-task install_cli", 30 | "publish": "smoke-task publish" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^14.14.41", 34 | "prettier": "^2.7.1", 35 | "smoke-run": "^1.4.0", 36 | "smoke-task": "^1.1.4", 37 | "typescript": "^4.2.4" 38 | }, 39 | "dependencies": { 40 | "esbuild": "0.15.7" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Hammer

4 | 5 |

Build Tool for Browser and Node Applications

6 | 7 | [![npm version](https://badge.fury.io/js/%40sinclair%2Fhammer.svg)](https://badge.fury.io/js/%40sinclair%2Fhammer) 8 | 9 | 10 | 11 |
12 | 13 | ## Install 14 | 15 | ```shell 16 | $ npm install @sinclair/hammer -g 17 | ``` 18 | 19 | ## Usage 20 | 21 | Create an `index.html` file 22 | ```html 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ``` 34 | Run Hammer 35 | ```shell 36 | $ hammer build index.html 37 | ``` 38 | Done 39 | 40 | ## Overview 41 | 42 | Hammer is a command line tool for browser and node application development. It provides a command line interface to trivially run both browser and node applications and offers appropriate `watch` and `reload` workflows for each environment. It is designed with rapid application development in mind and requires little to no configuration to use. 43 | 44 | Hammer was written to consolidate several disparate tools related to monitoring node processes (nodemon), building from HTML (parcel), mono repository support (lerna, nx) and project automation (gulp, grunt). It takes `esbuild` as its only dependency and is as much concerned with build performance as it is with dramatically reducing the number of development dependencies required for modern web application development. 45 | 46 | License MIT 47 | 48 | ## Serve 49 | 50 | Use the `serve` command to start a development server that reloads pages on save. 51 | 52 | ```html 53 | 54 | 55 | 56 | 57 | 58 | 59 |

Hello World

60 | 61 | 62 | ``` 63 | ```bash 64 | $ hammer serve index.html 65 | ``` 66 | 67 | ## Run 68 | 69 | Use the `run` command to start a node process that restarts on save. 70 | 71 | ```bash 72 | $ hammer run index.ts 73 | 74 | $ hammer run "index.ts arg1 arg2" # use quotes to pass arguments 75 | 76 | $ hammer run index.mts # node esm modules supported via .mts 77 | ``` 78 | 79 | ## Watch 80 | 81 | Use the `watch` command to start a compiler watch process only. 82 | 83 | ```bash 84 | $ hammer watch worker.ts 85 | ``` 86 | 87 | ## Monitor 88 | 89 | Use the `monitor` command to execute shell commands on file change. 90 | 91 | ```bash 92 | $ hammer monitor index.ts "deno run --allow-all index.ts" 93 | ``` 94 | 95 | ## Tasks 96 | 97 | Hammer provides a built-in task runner for automating various workflow at the command line. Tasks are created with JavaScript functions specified in a file named `hammer.mjs`. Hammer will search for the `hammer.mjs` file in the current working directory and setup a callable command line interface to each exported function. Hammer provides a global `shell(...)` function that can be used to start command line processes within each task. Additional functionality can be imported via ESM `import`. The following shows running a Hammer website and server watch process in parallel. 98 | 99 | ```typescript 100 | // 101 | // file: hammer.mjs 102 | // 103 | export async function start() { 104 | await Promise.all([ 105 | shell(`hammer serve apps/website/index.html --dist dist/website`), 106 | shell(`hammer run apps/server/index.ts --dist dist/server`) 107 | ]) 108 | } 109 | ``` 110 | ```bash 111 | $ hammer task start 112 | ``` 113 | 114 | ## Libs 115 | 116 | In mono repository projects, you can import shared libraries by using TypeScript `tsconfig.json` path aliasing. 117 | 118 | ```shell 119 | /apps 120 | /server 121 | index.ts ───────────┐ 122 | /website │ 123 | index.html │ 124 | index.ts ───────────┤ depends on 125 | /libs │ 126 | /shared │ 127 | index.ts <──────────┘ 128 | tsconfig.json 129 | ``` 130 | To enable `website` and `server` to import the `shared` library. Configure `tsconfig.json` in the project root as follows. 131 | 132 | ```javascript 133 | { 134 | "compilerOptions": { 135 | "baseUrl": ".", 136 | "paths": { 137 | "@libs/shared": ["libs/shared/index.ts"], 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | Once configured the `server` and `website` applications can import with the following. 144 | 145 | ```typescript 146 | import { Foo } from '@libs/shared' 147 | ``` 148 | 149 | ## Cli 150 | 151 | Hammer provides the following command line interface. 152 | 153 | ``` 154 | Commands: 155 | 156 | $ hammer run ' 120 | const lines = content.split('\n') 121 | for (let i = lines.length - 1; i >= 0; i--) { 122 | const current = lines[i] 123 | if (current.includes(' 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | export * from './shell' 28 | -------------------------------------------------------------------------------- /src/shell/shell.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | import { ChildProcess, spawn, spawnSync, execSync } from 'child_process' 28 | 29 | // ------------------------------------------------------------------------------- 30 | // Shell 31 | // ------------------------------------------------------------------------------- 32 | 33 | export interface IShell { 34 | wait(): Promise 35 | dispose(): void 36 | } 37 | 38 | // ------------------------------------------------------------------------------- 39 | // WindowsShell 40 | // ------------------------------------------------------------------------------- 41 | 42 | export class WindowsShell implements IShell { 43 | private readonly cp: ChildProcess 44 | private promise!: Promise 45 | private resolve!: (value: number | null) => void 46 | private disposed: boolean 47 | private exited: boolean 48 | 49 | constructor(private readonly command: string, stdio: 'inherit' | 'ignore') { 50 | this.promise = new Promise((resolve) => { 51 | this.resolve = resolve 52 | }) 53 | const [cmd, params] = this.parseArguments(this.command) 54 | this.cp = spawn(cmd, params, { stdio }) 55 | this.cp.on('close', (code) => this.onClose(code)) 56 | this.cp.on('exit', (code) => this.onExit(code)) 57 | this.disposed = false 58 | this.exited = false 59 | } 60 | 61 | // ------------------------------------------------- 62 | // Methods 63 | // ------------------------------------------------- 64 | 65 | public wait(): Promise { 66 | return this.promise 67 | } 68 | 69 | public dispose() { 70 | if (!this.exited && !this.disposed) { 71 | this.disposed = true 72 | this.terminate() 73 | } 74 | } 75 | 76 | // ------------------------------------------------- 77 | // Events 78 | // ------------------------------------------------- 79 | 80 | private onExit(code: number | null) { 81 | this.resolve(code) 82 | this.exited = true 83 | } 84 | 85 | private onClose(code: number | null) { 86 | this.resolve(code) 87 | if (!this.exited) this.terminate() 88 | } 89 | 90 | // ------------------------------------------------- 91 | // Terminate 92 | // ------------------------------------------------- 93 | 94 | private terminate() { 95 | try { 96 | execSync(`taskkill /pid ${this.cp.pid} /T /F`) 97 | } catch (error) { 98 | if (error instanceof Error) { 99 | console.warn(error.message) 100 | return 101 | } 102 | console.warn(error) 103 | } 104 | } 105 | 106 | // ------------------------------------------------- 107 | // Parser 108 | // ------------------------------------------------- 109 | 110 | private parseArguments(command: string): [string, string[]] { 111 | const chars = command.split('') 112 | const args: string[] = [] 113 | const temp: string[] = [] 114 | while (chars.length > 0) { 115 | const char = chars.shift()! 116 | switch (char) { 117 | case '"': { 118 | const result = this.consumeCharsAsString(chars, char) 119 | args.push(result) 120 | break 121 | } 122 | case "'": { 123 | const result = this.consumeCharsAsString(chars, char) 124 | args.push(result) 125 | break 126 | } 127 | case ' ': { 128 | const result = this.consumeChars(temp) 129 | if (result.length > 0) args.push(result) 130 | break 131 | } 132 | default: { 133 | temp.push(char) 134 | break 135 | } 136 | } 137 | } 138 | const result = this.consumeChars(temp) 139 | if (result.length > 0) args.push(result) 140 | return ['cmd', ['/c', ...args]] 141 | } 142 | 143 | private consumeChars(chars: string[]): string { 144 | const result = chars.join('') 145 | while (chars.length > 0) chars.shift() 146 | return result 147 | } 148 | 149 | private consumeCharsAsString(buffer: string[], close: string): string { 150 | const result: string[] = [] 151 | while (buffer.length > 0) { 152 | const char = buffer.shift()! 153 | if (char === close) return result.join('') 154 | else result.push(char) 155 | } 156 | return result.join('') 157 | } 158 | } 159 | 160 | // ------------------------------------------------------------------------------- 161 | // LinuxShell 162 | // ------------------------------------------------------------------------------- 163 | 164 | export class LinuxShell implements IShell { 165 | private readonly cp: ChildProcess 166 | private promise!: Promise 167 | private resolve!: (value: number | null) => void 168 | private disposed: boolean 169 | private exited: boolean 170 | 171 | constructor(private readonly command: string, stdio: 'inherit' | 'ignore') { 172 | this.promise = new Promise((resolve) => { 173 | this.resolve = resolve 174 | }) 175 | const [cmd, params] = this.parseArguments(this.command) 176 | this.cp = spawn(cmd, params, { stdio }) 177 | this.cp.on('close', (code) => this.onClose(code)) 178 | this.cp.on('exit', (code) => this.onExit(code)) 179 | this.disposed = false 180 | this.exited = false 181 | } 182 | 183 | // ------------------------------------------------- 184 | // Methods 185 | // ------------------------------------------------- 186 | 187 | public wait(): Promise { 188 | return this.promise 189 | } 190 | 191 | public dispose() { 192 | if (!this.exited && !this.disposed) { 193 | this.disposed = true 194 | this.terminate() 195 | } 196 | } 197 | 198 | // ------------------------------------------------- 199 | // Events 200 | // ------------------------------------------------- 201 | 202 | private onExit(code: number | null) { 203 | this.resolve(code) 204 | this.exited = true 205 | } 206 | 207 | private onClose(code: number | null) { 208 | this.resolve(code) 209 | if (!this.exited) this.terminate() 210 | } 211 | 212 | // ------------------------------------------------- 213 | // Terminate 214 | // ------------------------------------------------- 215 | 216 | private terminate() { 217 | try { 218 | const params = ['-o', 'pid', '--no-headers', '--ppid', this.cp.pid!.toString()] 219 | const result = spawnSync('ps', params, { encoding: 'utf8' }) 220 | const pid = parseInt(result.output[1]!) 221 | process.kill(pid, 'SIGTERM') 222 | this.cp.kill() 223 | } catch (error) { 224 | if (error instanceof Error) { 225 | console.warn(error.message) 226 | return 227 | } 228 | console.warn(error) 229 | } 230 | } 231 | 232 | // ------------------------------------------------- 233 | // Parser 234 | // ------------------------------------------------- 235 | 236 | private parseArguments(command: string): [string, string[]] { 237 | return ['sh', ['-c', command]] 238 | } 239 | } 240 | 241 | // ------------------------------------------------------------------------------- 242 | // Shell 243 | // ------------------------------------------------------------------------------- 244 | 245 | export class Shell implements IShell { 246 | private readonly shell: IShell 247 | constructor(command: string, stdio: 'inherit' | 'ignore') { 248 | this.shell = /^win/.test(process.platform) ? new WindowsShell(command, stdio) : new LinuxShell(command, stdio) 249 | } 250 | public wait(): Promise { 251 | return this.shell.wait() 252 | } 253 | 254 | public dispose(): void { 255 | return this.shell.dispose() 256 | } 257 | } 258 | 259 | export interface ShellOptions { 260 | /** Expected Exit Code */ 261 | expect?: number 262 | /** Suppress stdio */ 263 | stdio?: boolean 264 | } 265 | 266 | /** 267 | * Executes the given shell command and returns its exitcode. This function will inherit the 268 | * stdio interfaces of the the host process. This function will throw if the processes exitcode 269 | * does not equal the expected value. If left undefined, this function will resolve successfully 270 | * irrespective of if the process crashed. 271 | */ 272 | export async function shell(command: string, options: ShellOptions = {}): Promise { 273 | const stdio = options.stdio !== undefined && options.stdio === false ? 'ignore' : 'inherit' 274 | const expect = options.expect !== undefined ? options.expect : 0 275 | const shell = new Shell(command, stdio) 276 | const exitcode = await shell.wait() 277 | if (expect === undefined) return exitcode 278 | if (expect !== exitcode) throw new Error(`The shell command '${command}' exited with code ${exitcode} but expected code ${expect}.`) 279 | return exitcode 280 | } 281 | -------------------------------------------------------------------------------- /src/task/global/delay.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | /** Returns a promise that resolves after the given number of milliseconds. */ 28 | export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 29 | -------------------------------------------------------------------------------- /src/task/global/error.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | export class SystemError extends Error {} 28 | -------------------------------------------------------------------------------- /src/task/global/file.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | import { SystemError } from './error' 28 | import * as fs from './fs' 29 | import * as path from 'path' 30 | import * as crypto from 'crypto' 31 | 32 | export class FileError extends SystemError { 33 | constructor(public readonly operation: string, public readonly reason: string) { 34 | super(`File.${operation}(...): ${reason}`) 35 | } 36 | } 37 | 38 | export class File { 39 | constructor(private readonly filePath: string) {} 40 | 41 | /** Appends this file with the given data. If the file does not exist it will be created. */ 42 | public async append(data: Buffer | string): Promise { 43 | if (!(await this.checkExists(this.filePath))) { 44 | await this.createFolder(path.dirname(this.filePath)) 45 | await fs.writeFile(this.filePath, Buffer.alloc(0)) 46 | } 47 | await fs.appendFile(this.filePath, data) 48 | } 49 | 50 | /** Copies this file into the target folder. If the target folder does not exist it will be created. */ 51 | public async copy(folderPath: string): Promise { 52 | await this.assertExists('copyTo', this.filePath) 53 | await this.createFolder(folderPath) 54 | const targetPath = path.join(folderPath, path.basename(this.filePath)) 55 | await fs.copyFile(this.filePath, targetPath) 56 | } 57 | 58 | /** Creates an empty file if not exists. */ 59 | public async create(): Promise { 60 | if (await this.checkExists(this.filePath)) return 61 | await this.createFolder(path.dirname(this.filePath)) 62 | await fs.writeFile(this.filePath, Buffer.alloc(0)) 63 | } 64 | 65 | /** Deletes this file if it exists. Otherwise no action. */ 66 | public async delete(): Promise { 67 | if (!(await this.checkExists(this.filePath))) return 68 | await fs.unlink(this.filePath) 69 | } 70 | 71 | /** Replaces text content in this file that matches the given string or regular expression. */ 72 | public async edit(pattern: RegExp | string, replacement: string): Promise { 73 | await this.assertExists('edit', this.filePath) 74 | const content = await fs.readFile(this.filePath, 'utf-8') 75 | const search = typeof pattern === 'string' ? new RegExp(pattern, 'g') : pattern 76 | const updated = content.replace(search, replacement) 77 | await fs.writeFile(this.filePath, updated) 78 | } 79 | 80 | /** Returns true if this file exists. */ 81 | public async exists(): Promise { 82 | if (!(await this.checkExists(this.filePath))) return false 83 | const stat = await fs.stat(this.filePath) 84 | return stat.isFile() 85 | } 86 | 87 | /** Returns a hash for this file with the given algorithm (default is sha1, digest is hex) */ 88 | public async hash(algorithm: string = 'sha1', digest: crypto.BinaryToTextEncoding = 'hex') { 89 | await this.assertExists('hash', this.filePath) 90 | let offset = 0 91 | const fd = await fs.open(this.filePath, 'r') 92 | const hash = crypto.createHash(algorithm) 93 | const buffer = Buffer.alloc(16384) 94 | while (true) { 95 | const { bytesRead } = await fs.read(fd, buffer, 0, buffer.length, offset) 96 | if (bytesRead === 0) break 97 | offset = offset + bytesRead 98 | hash.update(buffer) 99 | } 100 | await fs.close(fd) 101 | return hash.digest(digest) 102 | } 103 | 104 | /** Moves this file into the target folder. If the target folder does not exist it will be created. */ 105 | public async move(folderPath: string): Promise { 106 | await this.assertExists('moveTo', this.filePath) 107 | await this.createFolder(folderPath) 108 | const targetPath = path.join(folderPath, path.basename(this.filePath)) 109 | await fs.copyFile(this.filePath, targetPath) 110 | await fs.unlink(this.filePath) 111 | } 112 | 113 | /** Prepends this file with the given data. If the file does not exist it will be created. */ 114 | public async prepend(data: Buffer | string): Promise { 115 | if (!(await this.checkExists(this.filePath))) { 116 | return await fs.writeFile(this.filePath, Buffer.from(data)) 117 | } 118 | const sources = [Buffer.from(data), await fs.readFile(this.filePath)] 119 | const buffer = Buffer.concat(sources) 120 | await fs.writeFile(this.filePath, buffer) 121 | } 122 | 123 | /** Reads this file as a JSON object. Will throw if the content cannot be parsed. */ 124 | public async json(): Promise { 125 | await this.assertExists('json', this.filePath) 126 | const content = await fs.readFile(this.filePath, 'utf-8') 127 | try { 128 | return JSON.parse(content) 129 | } catch {} 130 | throw new FileError('json', `The file path '${this.filePath}' failed to parse as json.`) 131 | } 132 | 133 | /** Returns the absolute path of this file. */ 134 | public path(): string { 135 | return path.resolve(this.filePath) 136 | } 137 | 138 | /** Reads this file as a buffer. */ 139 | public async read(): Promise 140 | 141 | /** Reads this file with the given encoding. */ 142 | public async read(encoding: BufferEncoding): Promise 143 | 144 | /** Reads the contents of this file. */ 145 | public async read(...args: any[]): Promise { 146 | await this.assertExists('read', this.filePath) 147 | const encoding = args.length === 0 ? 'binary' : (args[0] as BufferEncoding) 148 | return await fs.readFile(this.filePath, encoding) 149 | } 150 | 151 | /** Renames this file to the given newname. */ 152 | public async rename(newName: string): Promise { 153 | await this.assertExists('rename', this.filePath) 154 | const targetPath = path.join(path.dirname(this.filePath), newName) 155 | await fs.rename(this.filePath, targetPath) 156 | } 157 | 158 | /** Returns the size of this file in bytes. */ 159 | public async size(): Promise { 160 | await this.assertExists('size', this.filePath) 161 | const stat = await fs.stat(this.filePath) 162 | return stat.size 163 | } 164 | 165 | /** Returns the stats object for this file. */ 166 | public async stat() { 167 | await this.assertExists('stat', this.filePath) 168 | return await fs.stat(this.filePath) 169 | } 170 | 171 | /** Truncates the contents of this file. If the file does not exist, it is created. */ 172 | public async truncate(length: number = 0): Promise { 173 | if (!(await this.checkExists(this.filePath))) { 174 | await this.createFolder(path.dirname(this.filePath)) 175 | return await fs.writeFile(this.filePath, Buffer.alloc(length)) 176 | } 177 | return await fs.truncate(this.filePath, length) 178 | } 179 | 180 | /** Writes to this file. If the file does not exist, it is created. */ 181 | public async write(data: string | Buffer): Promise { 182 | await this.createFolder(path.dirname(this.filePath)) 183 | return await fs.writeFile(this.filePath, data) 184 | } 185 | 186 | /** Asserts the given path exists. */ 187 | private async assertExists(operation: string, systemPath: string) { 188 | if (await this.checkExists(systemPath)) return 189 | throw new FileError(operation, `The file path '${systemPath}' does not exist.`) 190 | } 191 | 192 | /** Checks the given path exists. */ 193 | private async checkExists(systemPath: string): Promise { 194 | return await fs 195 | .access(systemPath) 196 | .then(() => true) 197 | .catch(() => false) 198 | } 199 | 200 | /** Creates a directory if not exists. */ 201 | private async createFolder(folderPath: string): Promise { 202 | if (await this.checkExists(folderPath)) return 203 | await fs.mkdir(folderPath, { recursive: true }) 204 | } 205 | } 206 | 207 | /** Returns an interface to interact with a file. */ 208 | export function file(file: string): File { 209 | return new File(path.resolve(file)) 210 | } 211 | -------------------------------------------------------------------------------- /src/task/global/folder.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | import { SystemError } from './error' 28 | import { File } from './file' 29 | import * as fs from './fs' 30 | import * as path from 'path' 31 | 32 | export class FolderError extends SystemError { 33 | constructor(public readonly operation: string, public readonly reason: string) { 34 | super(`Folder.${operation}(...): ${reason}`) 35 | } 36 | } 37 | 38 | export class Folder { 39 | constructor(private readonly folderPath: string) {} 40 | 41 | /** Returns an interface to interact with a folders contents. */ 42 | public contents(): Contents { 43 | return new Contents(this.folderPath) 44 | } 45 | 46 | /** 47 | * Copies a file or folder into this folder. If this folder does not exist, it will be created. 48 | * Any existing files copied into this folder will be overwritten. 49 | */ 50 | public async add(systemPath: string) { 51 | await this.createFolder(this.folderPath) 52 | if (!(await this.checkExists(systemPath))) return 53 | const stat = await fs.stat(systemPath) 54 | if (stat.isDirectory()) return await new Folder(systemPath).copy(this.folderPath) 55 | if (stat.isFile()) return await new File(systemPath).copy(this.folderPath) 56 | } 57 | 58 | /** Copies this folder to a target folder. */ 59 | public async copy(folderPath: string) { 60 | await this.assertExists('copy', this.folderPath) 61 | await this.createFolder(folderPath) 62 | for (const partialPath of await fs.readdir(this.folderPath)) { 63 | const absolutePath = path.resolve(this.folderPath) 64 | const sourcePath = path.resolve(path.join(this.folderPath, partialPath)) 65 | const deltaPath = sourcePath.replace(absolutePath, '') 66 | const targetPath = path.resolve(path.join(folderPath, path.basename(absolutePath), deltaPath)) 67 | const stat = await fs.stat(sourcePath) 68 | if (stat.isDirectory()) { 69 | await fs.mkdir(targetPath, { recursive: true }) 70 | const folder = new Folder(sourcePath) 71 | await folder.copy(path.dirname(targetPath)) 72 | } 73 | if (stat.isFile()) { 74 | await this.createFolder(path.dirname(targetPath)) 75 | await fs.copyFile(sourcePath, targetPath) 76 | } 77 | } 78 | } 79 | 80 | /** Creates this folder. If the folder exists, no action. */ 81 | public async create(): Promise { 82 | if (await this.checkExists(this.folderPath)) return 83 | await fs.mkdir(this.folderPath, { recursive: true }) 84 | } 85 | 86 | /** Deletes this folder. If the folder does not exist, no action. */ 87 | public async delete(): Promise { 88 | if (!(await this.checkExists(this.folderPath))) return 89 | await fs.rm(this.folderPath, { recursive: true, force: true }) 90 | } 91 | 92 | /** Returns true if this folder exists. */ 93 | public async exists(): Promise { 94 | if (!(await this.checkExists(this.folderPath))) return false 95 | const stat = await fs.stat(this.folderPath) 96 | return stat.isDirectory() 97 | } 98 | 99 | /** Moves this folder to a target folder. */ 100 | public async move(folderPath: string): Promise { 101 | await this.assertExists('move', this.folderPath) 102 | await new Folder(this.folderPath).copy(folderPath) 103 | await new Folder(this.folderPath).delete() 104 | } 105 | 106 | /** Renames this folder. */ 107 | public async rename(newName: string): Promise { 108 | await this.assertExists('rename', this.folderPath) 109 | const folderPath = path.dirname(this.folderPath) 110 | await fs.rename(this.folderPath, path.join(folderPath, newName)) 111 | } 112 | 113 | /** Returns the size of this folder in bytes. */ 114 | public async size(): Promise { 115 | await this.assertExists('size', this.folderPath) 116 | let size = 0 117 | for await (const file of this.enumerateFiles()) { 118 | size = size + (await file.size()) 119 | } 120 | return size 121 | } 122 | 123 | /** Returns a stats object for this folder. */ 124 | public async stat() { 125 | await this.assertExists('stat', this.folderPath) 126 | await fs.stat(this.folderPath) 127 | } 128 | 129 | /** Asserts the given system path exists. */ 130 | private async assertExists(operation: string, systemPath: string) { 131 | if (await this.checkExists(systemPath)) return 132 | throw new FolderError(operation, `The folder path '${systemPath}' does not exist.`) 133 | } 134 | 135 | private async checkExists(systemPath: string): Promise { 136 | return await fs 137 | .access(systemPath) 138 | .then(() => true) 139 | .catch(() => false) 140 | } 141 | 142 | private async createFolder(folderPath: string): Promise { 143 | if (await this.checkExists(folderPath)) return 144 | await fs.mkdir(folderPath, { recursive: true }) 145 | } 146 | 147 | /** Recursively enumerates all files within this directory. */ 148 | private async *enumerateFiles(): AsyncGenerator { 149 | await this.assertExists('enumerate', this.folderPath) 150 | for (const partialPath of await fs.readdir(this.folderPath)) { 151 | const sourcePath = path.join(this.folderPath, partialPath) 152 | const stat = await fs.stat(sourcePath) 153 | if (stat.isDirectory()) yield* new Folder(sourcePath).enumerateFiles() 154 | if (stat.isFile()) yield new File(sourcePath) 155 | } 156 | } 157 | } 158 | 159 | export class ContentsError extends SystemError { 160 | constructor(public readonly operation: string, public readonly reason: string) { 161 | super(`Contents.${operation}(...): ${reason}`) 162 | } 163 | } 164 | 165 | export class Contents { 166 | constructor(private folderPath: string) {} 167 | 168 | /** Copies the contents for this folder into a target folder. */ 169 | public async copy(folderPath: string): Promise { 170 | await this.assertExists('copy', this.folderPath) 171 | await this.createFolder(folderPath) 172 | for (const partialPath of await fs.readdir(this.folderPath)) { 173 | const absolutePath = path.resolve(this.folderPath) 174 | const sourcePath = path.resolve(path.join(this.folderPath, partialPath)) 175 | const deltaPath = sourcePath.replace(absolutePath, '') 176 | const targetPath = path.resolve(path.join(folderPath, deltaPath)) 177 | const stat = await fs.stat(sourcePath) 178 | if (stat.isDirectory()) await new Folder(sourcePath).copy(path.dirname(targetPath)) 179 | if (stat.isFile()) await new File(sourcePath).copy(targetPath) 180 | } 181 | } 182 | 183 | /** Copies the contents for this folder into a target folder. */ 184 | public async move(folderPath: string): Promise { 185 | await this.assertExists('move', this.folderPath) 186 | await this.createFolder(folderPath) 187 | for (const partialPath of await fs.readdir(this.folderPath)) { 188 | const absolutePath = path.resolve(this.folderPath) 189 | const sourcePath = path.resolve(path.join(this.folderPath, partialPath)) 190 | const deltaPath = sourcePath.replace(absolutePath, '') 191 | const targetPath = path.join(folderPath, deltaPath) 192 | const stat = await fs.stat(sourcePath) 193 | if (stat.isDirectory()) await new Folder(sourcePath).copy(path.dirname(targetPath)) 194 | if (stat.isFile()) await new File(sourcePath).copy(targetPath) 195 | } 196 | for (const partialPath of await fs.readdir(this.folderPath)) { 197 | const targetPath = path.join(this.folderPath, partialPath) 198 | await fs.rm(targetPath, { recursive: true, force: true }) 199 | } 200 | } 201 | 202 | private async createFolder(folderPath: string): Promise { 203 | if (await this.checkExists(folderPath)) return 204 | await fs.mkdir(folderPath, { recursive: true }) 205 | } 206 | 207 | private async assertExists(operation: string, systemPath: string) { 208 | if (await this.checkExists(systemPath)) return 209 | throw new ContentsError(operation, `The folder path '${systemPath}' does not exist.`) 210 | } 211 | 212 | private async checkExists(systemPath: string): Promise { 213 | return await fs 214 | .access(systemPath) 215 | .then(() => true) 216 | .catch(() => false) 217 | } 218 | } 219 | 220 | /** Returns an interface to interact with a folder. */ 221 | export function folder(folderPath: string): Folder { 222 | return new Folder(path.resolve(folderPath)) 223 | } 224 | -------------------------------------------------------------------------------- /src/task/global/fs.ts: -------------------------------------------------------------------------------- 1 | import { folder_delete } from './supports/supports' 2 | import * as fs from 'fs' 3 | import * as util from 'util' 4 | 5 | // ------------------------------------------------------- 6 | // node 12 7 | // ------------------------------------------------------- 8 | 9 | export const appendFile = util.promisify(fs.appendFile) 10 | export const access = util.promisify(fs.access) 11 | export const close = util.promisify(fs.close) 12 | export const copyFile = util.promisify(fs.copyFile) 13 | export const exists = util.promisify(fs.exists) 14 | export const mkdir = util.promisify(fs.mkdir) 15 | export const open = util.promisify(fs.open) 16 | export const readdir = util.promisify(fs.readdir) 17 | export const read = util.promisify(fs.read) 18 | export const readFile = util.promisify(fs.readFile) 19 | export const stat = util.promisify(fs.stat) 20 | export const truncate = util.promisify(fs.truncate) 21 | export const rename = util.promisify(fs.rename) 22 | export const unlink = util.promisify(fs.unlink) 23 | export const write = util.promisify(fs.write) 24 | export const writeFile = util.promisify(fs.writeFile) 25 | 26 | // ------------------------------------------------ 27 | // node 12+ 28 | // ------------------------------------------------ 29 | 30 | /** Recursive rm. By default. */ 31 | export const rm = async (path: string, options: { recursive: boolean; force: boolean }): Promise => { 32 | if (!fs.existsSync(path)) return 33 | const stat = fs.statSync(path) 34 | if (stat.isFile()) return await unlink(path) 35 | await folder_delete(path) 36 | } 37 | -------------------------------------------------------------------------------- /src/task/global/index.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | export * from './delay' 28 | export * from './file' 29 | export * from './folder' 30 | export * from './shell' 31 | export * from './watch' 32 | -------------------------------------------------------------------------------- /src/task/global/shell.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | export { shell } from '../../shell/index' 28 | -------------------------------------------------------------------------------- /src/task/global/supports/supports.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | import { existsSync, readdir, rmdir, stat, unlink, Stats } from 'fs' 28 | import { join, basename, resolve, relative } from 'path' 29 | import { promisify } from 'util' 30 | 31 | const readdirAsync = promisify(readdir) 32 | const rmdirAsync = promisify(rmdir) 33 | const unlinkAsync = promisify(unlink) 34 | const statAsync = promisify(stat) 35 | 36 | export interface Item { 37 | /** The folder used to produce this item. */ 38 | folder: string 39 | /** The path down level from this folder. */ 40 | path: string 41 | /** The stat object for this file or folder. */ 42 | stat: Stats 43 | } 44 | 45 | function compare(left: string, right: string): number { 46 | if (left > right) { 47 | return 1 48 | } 49 | if (left < right) { 50 | return -1 51 | } 52 | return 0 53 | } 54 | 55 | /** Returns true if this path is a folder that exists. */ 56 | export async function folder_exists(folder: string): Promise { 57 | if (!existsSync(folder)) return false 58 | const stat = await statAsync(folder) 59 | return stat.isDirectory() 60 | } 61 | 62 | /** 63 | * A specialized readdir that returns the {folder, path, stat} for the 64 | * given folder. The 'folder' component returns the absolute path for 65 | * the given folder passed to this function, and the path is the path 66 | * down level from the folder. Thus join(folder, path) yields the full 67 | * path of the item. The stat is a node stat object for the item 68 | * indicating either file or folder. 69 | */ 70 | export async function common_readdir(folder: string): Promise { 71 | const items = await Promise.all( 72 | ( 73 | await readdirAsync(folder) 74 | ).map(async (path) => ({ 75 | path: basename(path), 76 | folder: resolve(folder), 77 | stat: await statAsync(join(folder, basename(path))), 78 | })), 79 | ) 80 | const folders = items.filter((item) => item.stat.isDirectory()).sort((a, b) => compare(a.path, b.path)) 81 | const files = items.filter((item) => item.stat.isFile()).sort((a, b) => compare(a.path, b.path)) 82 | return [...folders, ...files] 83 | } 84 | 85 | async function read_folder(folder: string): Promise { 86 | const items: Item[] = [] 87 | for (const item of await common_readdir(folder)) { 88 | if (item.stat.isDirectory()) { 89 | items.push(item) 90 | items.push(...(await read_folder(join(item.folder, item.path)))) 91 | } 92 | if (item.stat.isFile()) { 93 | items.push(item) 94 | } 95 | } 96 | return items 97 | } 98 | /** Returns a recursive list of a folders contents. */ 99 | export async function common_readdir_flatmap(folder: string): Promise { 100 | return (await read_folder(folder)).map((item) => ({ 101 | folder: resolve(folder), 102 | path: relative(folder, join(item.folder, item.path)), 103 | stat: item.stat, 104 | })) 105 | } 106 | 107 | /** Deletes this folder. If not exists, then no action. */ 108 | export async function folder_delete(folder: string): Promise { 109 | if (await folder_exists(folder)) { 110 | const items = await common_readdir_flatmap(folder) 111 | const folders = items.filter((item) => item.stat.isDirectory()) 112 | const files = items.filter((item) => item.stat.isFile()) 113 | for (const item of files) { 114 | const target = join(item.folder, item.path) 115 | await unlinkAsync(target) 116 | } 117 | for (const item of folders.reverse()) { 118 | const target = join(item.folder, item.path) 119 | await rmdirAsync(target) 120 | } 121 | await rmdirAsync(folder) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/task/global/watch.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | export { watch } from '../../watch/index' 28 | -------------------------------------------------------------------------------- /src/task/index.ts: -------------------------------------------------------------------------------- 1 | export * from './task' 2 | -------------------------------------------------------------------------------- /src/task/task.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | import { file, folder, shell, watch, delay } from './global/index' 28 | import { evaluate } from '../evaluate/index' 29 | import * as fs from 'fs' 30 | 31 | function print(exports: any) { 32 | console.log() 33 | console.log('The following tasks are available') 34 | console.log() 35 | const keys = Object.keys(exports).filter((key) => typeof exports[key] === 'function') 36 | for (const key of keys) { 37 | console.log(` $ hammer task ${key}`) 38 | } 39 | console.log() 40 | } 41 | 42 | async function call(exports: any, name: string, params: any[]) { 43 | const task = exports[name] 44 | if (task === undefined) return print(exports) 45 | await task.apply(null, params) 46 | } 47 | 48 | /** Executes a task in the given scriptPath. */ 49 | export async function task(scriptPath: string, name: string, params: any[]) { 50 | if (!fs.existsSync(scriptPath)) { 51 | console.log(`Task: Task file 'hammer.mjs' not found.`) 52 | process.exit(1) 53 | } 54 | try { 55 | const exports = evaluate(scriptPath, { delay, file, folder, shell, watch }) 56 | await call(exports, name, params) 57 | } catch (error: any) { 58 | const message = error.message || error 59 | console.log(`Error: [${name}] ${message}`) 60 | process.exit(1) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "strict": true, 7 | "newLine": "lf" 8 | }, 9 | "files": ["cli.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/watch/index.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | export * from './watch' 28 | -------------------------------------------------------------------------------- /src/watch/watch.ts: -------------------------------------------------------------------------------- 1 | /*-------------------------------------------------------------------------- 2 | 3 | MIT License 4 | 5 | Copyright (c) Hammer 2022 Haydn Paterson (sinclair) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | ---------------------------------------------------------------------------*/ 26 | 27 | import { channel, Sender, Receiver, Debounce } from '../async/index' 28 | import * as path from 'path' 29 | import * as fs from 'fs' 30 | 31 | // -------------------------------------------------------------------------- 32 | // 33 | // Watcher 34 | // 35 | // A multiple file watcher. This class sets up fs watchers for the given 36 | // sourcePaths. If the sourcePath happens to be a directory and this watcher 37 | // is running on linux, then this may result in be multiple watchers per 38 | // sourcePath. This class will yield the sourcePath on change NOT the 39 | // internal file or directory that changed. 40 | // 41 | // +---- fs.Watcher 42 | // | 43 | // sourcePath --+---- fs.Watcher 44 | // | 45 | // +---- fs.Watcher 46 | // 47 | // +---- fs.Watcher 48 | // | 49 | // sourcePath --+---- fs.Watcher 50 | // | 51 | // +---- fs.Watcher 52 | // 53 | // -------------------------------------------------------------------------- 54 | 55 | type SourcePath = string 56 | 57 | export class Watcher { 58 | private readonly watchers: Map 59 | private readonly receiver: Receiver 60 | private readonly sender: Sender 61 | private readonly debounce: Debounce 62 | 63 | constructor(sourcePaths: string[]) { 64 | const [sender, receiver] = channel() 65 | this.debounce = new Debounce(100) 66 | this.watchers = new Map() 67 | this.sender = sender 68 | this.receiver = receiver 69 | this.add(sourcePaths) 70 | } 71 | 72 | // -------------------------------------------------------------- 73 | // Iterator 74 | // -------------------------------------------------------------- 75 | 76 | public async *[Symbol.asyncIterator]() { 77 | for await (const path of this.receiver) { 78 | yield path 79 | } 80 | } 81 | 82 | // -------------------------------------------------------------- 83 | // Methods 84 | // -------------------------------------------------------------- 85 | 86 | /** Adds additional sourcePaths to watch. If the sourcePath already exists in this watchers set, then no action will be taken. */ 87 | public add(sourcePaths: string[]) { 88 | for (const sourcePath of sourcePaths) { 89 | if (this.watchers.has(sourcePath)) continue 90 | if (!fs.existsSync(sourcePath)) continue 91 | this.watchers.set(sourcePath, [...this.createWatchers(sourcePath)]) 92 | } 93 | } 94 | 95 | /** Disposes of this watcher and terminates all internal watchers. */ 96 | public dispose(): void { 97 | this.sender.end() 98 | for (const [sourcePath, watchers] of this.watchers) { 99 | this.watchers.delete(sourcePath) 100 | for (const watcher of watchers) { 101 | watcher.close() 102 | } 103 | } 104 | } 105 | 106 | // -------------------------------------------------------------- 107 | // Events 108 | // -------------------------------------------------------------- 109 | 110 | private onChange(sourcePath: string) { 111 | this.debounce.run(() => this.sender.send(sourcePath)) 112 | } 113 | 114 | // -------------------------------------------------------------- 115 | // Watchers 116 | // -------------------------------------------------------------- 117 | 118 | private *createLinuxDirectoryWatchers(directory: string, sourcePath: string): Generator { 119 | const stat = fs.statSync(directory) 120 | yield fs.watch(directory, (event) => this.onChange(sourcePath)) 121 | for (const filepath of fs.readdirSync(directory)) { 122 | const next = path.join(directory, filepath) 123 | const stat = fs.statSync(next) 124 | if (stat.isDirectory()) yield* this.createLinuxDirectoryWatchers(next, sourcePath) 125 | } 126 | } 127 | 128 | private *createLinuxWatchers(sourcePath: string): Generator { 129 | const stat = fs.statSync(sourcePath) 130 | if (stat.isDirectory()) { 131 | yield* this.createLinuxDirectoryWatchers(sourcePath, sourcePath) 132 | } else if (stat.isFile()) { 133 | yield fs.watch(sourcePath, (event) => this.onChange(sourcePath)) 134 | } 135 | } 136 | 137 | private *createWindowsWatchers(sourcePath: string): Generator { 138 | const stat = fs.statSync(sourcePath) 139 | yield stat.isDirectory() ? fs.watch(sourcePath, { recursive: true }, (event) => this.onChange(sourcePath)) : fs.watch(sourcePath, (event) => this.onChange(sourcePath)) 140 | } 141 | 142 | private *createWatchers(sourcePath: string): Generator { 143 | yield* /^win/.test(process.platform) ? this.createWindowsWatchers(sourcePath) : this.createLinuxWatchers(sourcePath) 144 | } 145 | } 146 | 147 | export function watch(sourcePaths: string[]): Watcher { 148 | return new Watcher(sourcePaths) 149 | } 150 | -------------------------------------------------------------------------------- /tasks.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------------------------------------------------- 2 | // Clean 3 | // ------------------------------------------------------------------------------- 4 | export async function clean() { 5 | await folder('target').delete().exec() 6 | await folder('dist').delete().exec() 7 | } 8 | // ------------------------------------------------------------------------------- 9 | // Format 10 | // ------------------------------------------------------------------------------- 11 | export async function format() { 12 | await shell('prettier --no-semi --single-quote --print-width 240 --trailing-comma all --write src website').exec() 13 | } 14 | // ------------------------------------------------------------------------------- 15 | // Start 16 | // ------------------------------------------------------------------------------- 17 | export async function start(target = 'target/watch') { 18 | const options = 'serve website/index.html --dist target/website --serve 5000' 19 | await file(`${target}/cli.js`).create().exec() 20 | await Promise.all([ 21 | shell(`tsc --project src/tsconfig.json --outDir ${target} --watch`).exec(), 22 | shell(`smoke-run ${target} -x node ${target}/cli.js ${options}`).exec(), 23 | ]) 24 | } 25 | // ------------------------------------------------------------------------------- 26 | // Build 27 | // ------------------------------------------------------------------------------- 28 | export async function build(target = 'target/build') { 29 | await folder(`${target}`).delete().exec() 30 | await file(`${target}/index.js`).create().exec() 31 | await shell(`tsc --project src/tsconfig.json --outDir ${target} --declaration`).exec() 32 | await folder(`${target}`).add('src/hammer').exec() 33 | await folder(`${target}`).add('package.json').exec() 34 | await folder(`${target}`).add('license').exec() 35 | await folder(`${target}`).add('readme.md').exec() 36 | await shell(`cd ${target} && npm pack`).exec() 37 | } 38 | // ------------------------------------------------------------------------------- 39 | // Install (may require administrator) 40 | // ------------------------------------------------------------------------------- 41 | export async function install_cli(target = 'target/build') { 42 | await build() 43 | const packageJson = JSON.parse(await file('./package.json').read('utf-8')) 44 | await shell(`cd ${target} && npm install sinclair-hammer-${packageJson['version']}.tgz -g`).exec() 45 | } 46 | -------------------------------------------------------------------------------- /website/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Hammer Build Test: About

10 | 11 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /website/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Hammer Build Test: Contact

10 | 11 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /website/images/hammer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinclairzx81/hammer/7c475a932ae715fad4feeb1f11ac1a8a7660fb70/website/images/hammer.png -------------------------------------------------------------------------------- /website/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Hammer Build Test: Index

10 | 11 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /website/scripts/index.ts: -------------------------------------------------------------------------------- 1 | import { test } from './test' 2 | 3 | test() 4 | 5 | export class Foo {} 6 | -------------------------------------------------------------------------------- /website/scripts/test.ts: -------------------------------------------------------------------------------- 1 | export function test() { 2 | for (let i = 0; i < 10; i++) { 3 | console.log(i) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /website/server.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http' 2 | 3 | http.createServer((req, res) => res.end('Node Server Started')).listen(5001) 4 | 5 | console.log('Node Server Started', process.argv) 6 | -------------------------------------------------------------------------------- /website/styles/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #333; 3 | color: #aaa; 4 | font-family: monospace; 5 | } 6 | a { 7 | color: #aaa; 8 | } 9 | a:visited { 10 | color: #aaa; 11 | } 12 | --------------------------------------------------------------------------------