├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── LICENSE.md ├── README.md ├── cli.js ├── package-lock.json ├── package.json ├── src ├── actions │ ├── InitializePath.ts │ ├── PreventDefault.ts │ └── SetPathStatus.ts ├── components │ ├── Link.ts │ └── Router.ts ├── effects │ ├── loadRouteBundle.ts │ ├── loadStatic.ts │ └── navigate.ts ├── hyperstatic.ts ├── index.ts ├── renderPages.ts ├── subscriptions │ ├── onLinkEnteredViewPort.ts │ ├── onRouteChangeStart.ts │ └── onRouteChanged.ts ├── types.ts └── utils │ ├── htmlToVdom.ts │ ├── isPrerendering.ts │ ├── parseQueryString.ts │ └── provide.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # Change these settings to your own preference 9 | indent_style = space 10 | indent_size = 2 11 | 12 | # I recommend to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | # Other file types 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: ['standard'], 4 | rules: { 5 | 'no-undef': 0, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | 84 | 85 | ############################ 86 | # Node.js 87 | ############################ 88 | 89 | lib-cov 90 | lcov.info 91 | pids 92 | logs 93 | results 94 | node_modules 95 | .node_history 96 | 97 | 98 | ############################ 99 | # Tests 100 | ############################ 101 | 102 | testApp 103 | coverage 104 | 105 | 106 | 107 | ############################ 108 | # Parcel 109 | ############################ 110 | 111 | dist 112 | public 113 | *cache 114 | 115 | 116 | 117 | ############################ 118 | # Env 119 | ############################ 120 | 121 | .env 122 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 [Alexandre Lotte](dev@alexlotte.ca) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperstatic 2 | 3 | *The hyperapp static site generator* 4 | 5 | --- 6 | 7 | Hyperstatic is a small navigation layer on top of hyperapp that helps create fast and SEO friendly static sites. 8 | 9 | **The key features are:** 10 | - Routing and navigation 11 | - Prerendering 12 | - Prefetching at build-time 13 | - Code splitting 14 | 15 | See https://hyperstatic.dev/ 16 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./dist/renderPages').default() 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperstatic", 3 | "version": "2.1.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "14.14.28", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz", 10 | "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==" 11 | }, 12 | "@types/yargs": { 13 | "version": "16.0.0", 14 | "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.0.tgz", 15 | "integrity": "sha512-2nN6AGeMwe8+O6nO9ytQfbMQOJy65oi1yK2y/9oReR08DaXSGtMsrLyCM1ooKqfICpCx4oITaR4LkOmdzz41Ww==", 16 | "dev": true, 17 | "requires": { 18 | "@types/yargs-parser": "*" 19 | } 20 | }, 21 | "@types/yargs-parser": { 22 | "version": "20.2.0", 23 | "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", 24 | "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", 25 | "dev": true 26 | }, 27 | "@types/yauzl": { 28 | "version": "2.9.1", 29 | "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", 30 | "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", 31 | "optional": true, 32 | "requires": { 33 | "@types/node": "*" 34 | } 35 | }, 36 | "agent-base": { 37 | "version": "6.0.2", 38 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 39 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 40 | "requires": { 41 | "debug": "4" 42 | } 43 | }, 44 | "ansi-regex": { 45 | "version": "5.0.0", 46 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 47 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" 48 | }, 49 | "ansi-styles": { 50 | "version": "4.3.0", 51 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 52 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 53 | "requires": { 54 | "color-convert": "^2.0.1" 55 | } 56 | }, 57 | "at-least-node": { 58 | "version": "1.0.0", 59 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", 60 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" 61 | }, 62 | "balanced-match": { 63 | "version": "1.0.0", 64 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 65 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 66 | }, 67 | "base64-js": { 68 | "version": "1.5.1", 69 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 70 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 71 | }, 72 | "bl": { 73 | "version": "4.1.0", 74 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 75 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 76 | "requires": { 77 | "buffer": "^5.5.0", 78 | "inherits": "^2.0.4", 79 | "readable-stream": "^3.4.0" 80 | } 81 | }, 82 | "brace-expansion": { 83 | "version": "1.1.11", 84 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 85 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 86 | "requires": { 87 | "balanced-match": "^1.0.0", 88 | "concat-map": "0.0.1" 89 | } 90 | }, 91 | "buffer": { 92 | "version": "5.7.1", 93 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 94 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 95 | "requires": { 96 | "base64-js": "^1.3.1", 97 | "ieee754": "^1.1.13" 98 | } 99 | }, 100 | "buffer-crc32": { 101 | "version": "0.2.13", 102 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 103 | "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" 104 | }, 105 | "bytes": { 106 | "version": "3.0.0", 107 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 108 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 109 | }, 110 | "chalk": { 111 | "version": "4.1.0", 112 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 113 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 114 | "requires": { 115 | "ansi-styles": "^4.1.0", 116 | "supports-color": "^7.1.0" 117 | } 118 | }, 119 | "chownr": { 120 | "version": "1.1.4", 121 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 122 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 123 | }, 124 | "cliui": { 125 | "version": "7.0.4", 126 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 127 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 128 | "requires": { 129 | "string-width": "^4.2.0", 130 | "strip-ansi": "^6.0.0", 131 | "wrap-ansi": "^7.0.0" 132 | } 133 | }, 134 | "color-convert": { 135 | "version": "2.0.1", 136 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 137 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 138 | "requires": { 139 | "color-name": "~1.1.4" 140 | } 141 | }, 142 | "color-name": { 143 | "version": "1.1.4", 144 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 145 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 146 | }, 147 | "concat-map": { 148 | "version": "0.0.1", 149 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 150 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 151 | }, 152 | "content-disposition": { 153 | "version": "0.5.2", 154 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 155 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 156 | }, 157 | "debug": { 158 | "version": "4.3.1", 159 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", 160 | "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", 161 | "requires": { 162 | "ms": "2.1.2" 163 | } 164 | }, 165 | "devtools-protocol": { 166 | "version": "0.0.847576", 167 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.847576.tgz", 168 | "integrity": "sha512-0M8kobnSQE0Jmly7Mhbeq0W/PpZfnuK+WjN2ZRVPbGqYwCHCioAVp84H0TcLimgECcN5H976y5QiXMGBC9JKmg==" 169 | }, 170 | "emoji-regex": { 171 | "version": "8.0.0", 172 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 173 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 174 | }, 175 | "end-of-stream": { 176 | "version": "1.4.4", 177 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 178 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 179 | "requires": { 180 | "once": "^1.4.0" 181 | } 182 | }, 183 | "escalade": { 184 | "version": "3.1.1", 185 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 186 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" 187 | }, 188 | "extract-zip": { 189 | "version": "2.0.1", 190 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 191 | "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 192 | "requires": { 193 | "@types/yauzl": "^2.9.1", 194 | "debug": "^4.1.1", 195 | "get-stream": "^5.1.0", 196 | "yauzl": "^2.10.0" 197 | } 198 | }, 199 | "fast-url-parser": { 200 | "version": "1.1.3", 201 | "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", 202 | "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", 203 | "requires": { 204 | "punycode": "^1.3.2" 205 | } 206 | }, 207 | "fd-slicer": { 208 | "version": "1.1.0", 209 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 210 | "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", 211 | "requires": { 212 | "pend": "~1.2.0" 213 | } 214 | }, 215 | "find-up": { 216 | "version": "4.1.0", 217 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 218 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 219 | "requires": { 220 | "locate-path": "^5.0.0", 221 | "path-exists": "^4.0.0" 222 | } 223 | }, 224 | "fs-constants": { 225 | "version": "1.0.0", 226 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 227 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 228 | }, 229 | "fs-extra": { 230 | "version": "9.1.0", 231 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", 232 | "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", 233 | "requires": { 234 | "at-least-node": "^1.0.0", 235 | "graceful-fs": "^4.2.0", 236 | "jsonfile": "^6.0.1", 237 | "universalify": "^2.0.0" 238 | } 239 | }, 240 | "fs.realpath": { 241 | "version": "1.0.0", 242 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 243 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 244 | }, 245 | "get-caller-file": { 246 | "version": "2.0.5", 247 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 248 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 249 | }, 250 | "get-stream": { 251 | "version": "5.2.0", 252 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 253 | "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 254 | "requires": { 255 | "pump": "^3.0.0" 256 | } 257 | }, 258 | "glob": { 259 | "version": "7.1.6", 260 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 261 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 262 | "requires": { 263 | "fs.realpath": "^1.0.0", 264 | "inflight": "^1.0.4", 265 | "inherits": "2", 266 | "minimatch": "^3.0.4", 267 | "once": "^1.3.0", 268 | "path-is-absolute": "^1.0.0" 269 | } 270 | }, 271 | "graceful-fs": { 272 | "version": "4.2.6", 273 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", 274 | "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" 275 | }, 276 | "has-flag": { 277 | "version": "4.0.0", 278 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 279 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" 280 | }, 281 | "https-proxy-agent": { 282 | "version": "5.0.0", 283 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", 284 | "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", 285 | "requires": { 286 | "agent-base": "6", 287 | "debug": "4" 288 | } 289 | }, 290 | "hyperapp": { 291 | "version": "2.0.18", 292 | "resolved": "https://registry.npmjs.org/hyperapp/-/hyperapp-2.0.18.tgz", 293 | "integrity": "sha512-i9NoQtmg9vFy5iQVDlpWRahnJsMn8eb1WrkDXUkytHzxx7IfOdICU6UaZP6fv2FVXPnQSVxxpUo8HT8d5L+ktw==" 294 | }, 295 | "ieee754": { 296 | "version": "1.2.1", 297 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 298 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 299 | }, 300 | "inflight": { 301 | "version": "1.0.6", 302 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 303 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 304 | "requires": { 305 | "once": "^1.3.0", 306 | "wrappy": "1" 307 | } 308 | }, 309 | "inherits": { 310 | "version": "2.0.4", 311 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 312 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 313 | }, 314 | "is-fullwidth-code-point": { 315 | "version": "3.0.0", 316 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 317 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" 318 | }, 319 | "jsonfile": { 320 | "version": "6.1.0", 321 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 322 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 323 | "requires": { 324 | "graceful-fs": "^4.1.6", 325 | "universalify": "^2.0.0" 326 | } 327 | }, 328 | "locate-path": { 329 | "version": "5.0.0", 330 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 331 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 332 | "requires": { 333 | "p-locate": "^4.1.0" 334 | } 335 | }, 336 | "mime-db": { 337 | "version": "1.33.0", 338 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 339 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 340 | }, 341 | "mime-types": { 342 | "version": "2.1.18", 343 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 344 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 345 | "requires": { 346 | "mime-db": "~1.33.0" 347 | } 348 | }, 349 | "minimatch": { 350 | "version": "3.0.4", 351 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 352 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 353 | "requires": { 354 | "brace-expansion": "^1.1.7" 355 | } 356 | }, 357 | "mkdirp-classic": { 358 | "version": "0.5.3", 359 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 360 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 361 | }, 362 | "ms": { 363 | "version": "2.1.2", 364 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 365 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 366 | }, 367 | "node-fetch": { 368 | "version": "2.6.1", 369 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 370 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 371 | }, 372 | "once": { 373 | "version": "1.4.0", 374 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 375 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 376 | "requires": { 377 | "wrappy": "1" 378 | } 379 | }, 380 | "p-limit": { 381 | "version": "2.3.0", 382 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 383 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 384 | "requires": { 385 | "p-try": "^2.0.0" 386 | } 387 | }, 388 | "p-locate": { 389 | "version": "4.1.0", 390 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 391 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 392 | "requires": { 393 | "p-limit": "^2.2.0" 394 | } 395 | }, 396 | "p-try": { 397 | "version": "2.2.0", 398 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 399 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" 400 | }, 401 | "path-exists": { 402 | "version": "4.0.0", 403 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 404 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" 405 | }, 406 | "path-is-absolute": { 407 | "version": "1.0.1", 408 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 409 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 410 | }, 411 | "path-is-inside": { 412 | "version": "1.0.2", 413 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 414 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" 415 | }, 416 | "path-to-regexp": { 417 | "version": "6.2.0", 418 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.0.tgz", 419 | "integrity": "sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==" 420 | }, 421 | "pend": { 422 | "version": "1.2.0", 423 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 424 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" 425 | }, 426 | "pkg-dir": { 427 | "version": "4.2.0", 428 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 429 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 430 | "requires": { 431 | "find-up": "^4.0.0" 432 | } 433 | }, 434 | "progress": { 435 | "version": "2.0.3", 436 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 437 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" 438 | }, 439 | "proxy-from-env": { 440 | "version": "1.1.0", 441 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 442 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 443 | }, 444 | "pump": { 445 | "version": "3.0.0", 446 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 447 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 448 | "requires": { 449 | "end-of-stream": "^1.1.0", 450 | "once": "^1.3.1" 451 | } 452 | }, 453 | "punycode": { 454 | "version": "1.4.1", 455 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 456 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 457 | }, 458 | "puppeteer": { 459 | "version": "7.1.0", 460 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-7.1.0.tgz", 461 | "integrity": "sha512-lqOLzqCKdh7yUAHvK6LxgOpQrL8Bv1/jvS8MLDXxcNms2rlM3E8p/Wlwc7efbRZ0twxTzUeqjN5EqrTwxOwc9g==", 462 | "requires": { 463 | "debug": "^4.1.0", 464 | "devtools-protocol": "0.0.847576", 465 | "extract-zip": "^2.0.0", 466 | "https-proxy-agent": "^5.0.0", 467 | "node-fetch": "^2.6.1", 468 | "pkg-dir": "^4.2.0", 469 | "progress": "^2.0.1", 470 | "proxy-from-env": "^1.1.0", 471 | "rimraf": "^3.0.2", 472 | "tar-fs": "^2.0.0", 473 | "unbzip2-stream": "^1.3.3", 474 | "ws": "^7.2.3" 475 | } 476 | }, 477 | "range-parser": { 478 | "version": "1.2.0", 479 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 480 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 481 | }, 482 | "readable-stream": { 483 | "version": "3.6.0", 484 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 485 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 486 | "requires": { 487 | "inherits": "^2.0.3", 488 | "string_decoder": "^1.1.1", 489 | "util-deprecate": "^1.0.1" 490 | } 491 | }, 492 | "replace-in-file": { 493 | "version": "6.2.0", 494 | "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.2.0.tgz", 495 | "integrity": "sha512-Im2AF9G/qgkYneOc9QwWwUS/efyyonTUBvzXS2VXuxPawE5yQIjT/e6x4CTijO0Quq48lfAujuo+S89RR2TP2Q==", 496 | "requires": { 497 | "chalk": "^4.1.0", 498 | "glob": "^7.1.6", 499 | "yargs": "^16.2.0" 500 | } 501 | }, 502 | "require-directory": { 503 | "version": "2.1.1", 504 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 505 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 506 | }, 507 | "rimraf": { 508 | "version": "3.0.2", 509 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 510 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 511 | "requires": { 512 | "glob": "^7.1.3" 513 | } 514 | }, 515 | "safe-buffer": { 516 | "version": "5.2.1", 517 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 518 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 519 | }, 520 | "serve-handler": { 521 | "version": "6.1.3", 522 | "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.3.tgz", 523 | "integrity": "sha512-FosMqFBNrLyeiIDvP1zgO6YoTzFYHxLDEIavhlmQ+knB2Z7l1t+kGLHkZIDN7UVWqQAmKI3D20A6F6jo3nDd4w==", 524 | "requires": { 525 | "bytes": "3.0.0", 526 | "content-disposition": "0.5.2", 527 | "fast-url-parser": "1.1.3", 528 | "mime-types": "2.1.18", 529 | "minimatch": "3.0.4", 530 | "path-is-inside": "1.0.2", 531 | "path-to-regexp": "2.2.1", 532 | "range-parser": "1.2.0" 533 | }, 534 | "dependencies": { 535 | "path-to-regexp": { 536 | "version": "2.2.1", 537 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", 538 | "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" 539 | } 540 | } 541 | }, 542 | "string-width": { 543 | "version": "4.2.2", 544 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", 545 | "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", 546 | "requires": { 547 | "emoji-regex": "^8.0.0", 548 | "is-fullwidth-code-point": "^3.0.0", 549 | "strip-ansi": "^6.0.0" 550 | } 551 | }, 552 | "string_decoder": { 553 | "version": "1.3.0", 554 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 555 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 556 | "requires": { 557 | "safe-buffer": "~5.2.0" 558 | } 559 | }, 560 | "strip-ansi": { 561 | "version": "6.0.0", 562 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 563 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 564 | "requires": { 565 | "ansi-regex": "^5.0.0" 566 | } 567 | }, 568 | "supports-color": { 569 | "version": "7.2.0", 570 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 571 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 572 | "requires": { 573 | "has-flag": "^4.0.0" 574 | } 575 | }, 576 | "tar-fs": { 577 | "version": "2.1.1", 578 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 579 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 580 | "requires": { 581 | "chownr": "^1.1.1", 582 | "mkdirp-classic": "^0.5.2", 583 | "pump": "^3.0.0", 584 | "tar-stream": "^2.1.4" 585 | } 586 | }, 587 | "tar-stream": { 588 | "version": "2.2.0", 589 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 590 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 591 | "requires": { 592 | "bl": "^4.0.3", 593 | "end-of-stream": "^1.4.1", 594 | "fs-constants": "^1.0.0", 595 | "inherits": "^2.0.3", 596 | "readable-stream": "^3.1.1" 597 | } 598 | }, 599 | "through": { 600 | "version": "2.3.8", 601 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 602 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 603 | }, 604 | "typescript": { 605 | "version": "4.2.4", 606 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", 607 | "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", 608 | "dev": true 609 | }, 610 | "unbzip2-stream": { 611 | "version": "1.4.3", 612 | "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", 613 | "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", 614 | "requires": { 615 | "buffer": "^5.2.1", 616 | "through": "^2.3.8" 617 | } 618 | }, 619 | "universalify": { 620 | "version": "2.0.0", 621 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", 622 | "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" 623 | }, 624 | "util-deprecate": { 625 | "version": "1.0.2", 626 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 627 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 628 | }, 629 | "wrap-ansi": { 630 | "version": "7.0.0", 631 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 632 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 633 | "requires": { 634 | "ansi-styles": "^4.0.0", 635 | "string-width": "^4.1.0", 636 | "strip-ansi": "^6.0.0" 637 | } 638 | }, 639 | "wrappy": { 640 | "version": "1.0.2", 641 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 642 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 643 | }, 644 | "ws": { 645 | "version": "7.4.3", 646 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.3.tgz", 647 | "integrity": "sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA==" 648 | }, 649 | "y18n": { 650 | "version": "5.0.5", 651 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", 652 | "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" 653 | }, 654 | "yargs": { 655 | "version": "16.2.0", 656 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 657 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 658 | "requires": { 659 | "cliui": "^7.0.2", 660 | "escalade": "^3.1.1", 661 | "get-caller-file": "^2.0.5", 662 | "require-directory": "^2.1.1", 663 | "string-width": "^4.2.0", 664 | "y18n": "^5.0.5", 665 | "yargs-parser": "^20.2.2" 666 | } 667 | }, 668 | "yargs-parser": { 669 | "version": "20.2.6", 670 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.6.tgz", 671 | "integrity": "sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==" 672 | }, 673 | "yauzl": { 674 | "version": "2.10.0", 675 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 676 | "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", 677 | "requires": { 678 | "buffer-crc32": "~0.2.3", 679 | "fd-slicer": "~1.1.0" 680 | } 681 | } 682 | } 683 | } 684 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.1", 3 | "name": "hyperstatic", 4 | "description": "Hyperapp static site framework", 5 | "author": "Alexandre Lotte", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "build": "tsc", 11 | "prepublish": "tsc", 12 | "test": "echo \"No test specified\"" 13 | }, 14 | "dependencies": { 15 | "fs-extra": "^9.1.0", 16 | "hyperapp": "^2.0.18", 17 | "path-to-regexp": "^6.2.0", 18 | "puppeteer": "^7.1.0", 19 | "replace-in-file": "^6.2.0", 20 | "serve-handler": "^6.1.3", 21 | "yargs": "^16.2.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^14.14.28", 25 | "@types/yargs": "^16.0.0", 26 | "typescript": "^4.2.4" 27 | }, 28 | "bin": { 29 | "hyperstatic": "./cli.js" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/loteoo/hyperstatic.git" 34 | }, 35 | "keywords": [ 36 | "hyperapp", 37 | "framework", 38 | "static-site-generator" 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/loteoo/hyperstatic/issues" 42 | }, 43 | "homepage": "https://hyperstatic.dev" 44 | } 45 | -------------------------------------------------------------------------------- /src/actions/InitializePath.ts: -------------------------------------------------------------------------------- 1 | import SetPathStatus from './SetPathStatus'; 2 | import { LocationState, State } from '../types'; 3 | 4 | interface InitializePathArgs { 5 | location: LocationState 6 | bundle: any; 7 | } 8 | 9 | /** 10 | * Run the "init" action if necessary once per page 11 | */ 12 | const InitializePath = (state: State, { location, bundle }: InitializePathArgs) => { 13 | const { path } = location; 14 | 15 | // If current path is already initiated, do nothing 16 | if (state.paths[path] === 'ready') { 17 | return state; 18 | } 19 | 20 | // If current path doesn't have an "Init" to run 21 | if (typeof bundle?.Init !== 'function' && typeof bundle?.init !== 'function') { 22 | 23 | // Set as ready 24 | return SetPathStatus(state, { path, status: 'ready' }) 25 | } 26 | 27 | const PageInitAction = bundle?.Init ?? bundle?.init 28 | 29 | // Compute next state or action tuple using the provided "Init" action 30 | const action = PageInitAction( 31 | state, 32 | location 33 | ) 34 | 35 | // If Init has side-effects 36 | if (Array.isArray(action)) { 37 | 38 | // Get only the "loadStatic" effect tuples 39 | const loadEffects = action.slice(1).filter((fx => fx[0].fxName === 'loadStatic')) 40 | 41 | // If this page has data requirements 42 | if (loadEffects.length > 0) { 43 | 44 | // Set path as fetching 45 | action[0] = SetPathStatus(action[0], { path, status: 'fetching' }) 46 | 47 | // Set the path for the effect 48 | loadEffects.forEach(fx => fx[1].path = path) 49 | } 50 | 51 | return action 52 | } 53 | 54 | return SetPathStatus(action, { path, status: 'ready' }) 55 | } 56 | 57 | export default InitializePath 58 | -------------------------------------------------------------------------------- /src/actions/PreventDefault.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../types'; 2 | 3 | const PreventDefault = (state: State, ev) => { 4 | ev.preventDefault(); 5 | return state; 6 | }; 7 | 8 | export default PreventDefault 9 | -------------------------------------------------------------------------------- /src/actions/SetPathStatus.ts: -------------------------------------------------------------------------------- 1 | import { PathStatus, State } from '../types' 2 | 3 | interface SetPathStatusArgs { 4 | path: string 5 | status: PathStatus; 6 | } 7 | 8 | /** 9 | * Set path status for a page 10 | */ 11 | const SetPathStatus = (state: State, { path, status }: SetPathStatusArgs): State => ({ 12 | ...state, 13 | paths: { 14 | ...state.paths, 15 | [path]: status 16 | } 17 | }) 18 | 19 | export default SetPathStatus 20 | -------------------------------------------------------------------------------- /src/components/Link.ts: -------------------------------------------------------------------------------- 1 | import { h, text } from "hyperapp"; 2 | import PreventDefault from "../actions/PreventDefault"; 3 | import navigate from "../effects/navigate"; 4 | import { PathInfo, State, ViewContext } from '../types'; 5 | 6 | const textTypes = ['string', 'number', 'bigint'] 7 | 8 | const childNode = (child) => 9 | textTypes.includes(typeof child) ? text(child) : child 10 | 11 | interface LinkProps { 12 | href: string; 13 | [x: string]: any; 14 | } 15 | 16 | /** 17 | * Link component to import in user code. 18 | * 19 | * Handles navigation, preloading and 20 | * offers info about targeted paths. 21 | * 22 | */ 23 | const Link = ({ href, ...rest }: LinkProps, children) => ({ 24 | state, 25 | options, 26 | getLocation, 27 | PreloadPage, 28 | }: ViewContext) => { 29 | const location = getLocation(href); 30 | const { route, path } = location; 31 | const status = state.paths[path] ?? "iddle"; 32 | const active = state.location.path === path; 33 | const navigateEventName = options.fastClicks ? "onmousedown" : "onclick"; 34 | const renderChildren = (child) => { 35 | if (typeof child === "function") { 36 | const info: PathInfo = { 37 | ...location, 38 | status, 39 | active, 40 | }; 41 | return childNode(child(info)); 42 | } 43 | return childNode(child); 44 | }; 45 | 46 | // External link 47 | if (!href.startsWith('/')) { 48 | return h( 49 | "a", 50 | { 51 | href, 52 | ...rest, 53 | }, 54 | [...children].map(renderChildren) 55 | ); 56 | } 57 | 58 | // 404 link 59 | if (!route) { 60 | if (process.env.NODE_ENV === "development") { 61 | console.warn(`Invalid link pointing to ${href} will 404.`); 62 | } 63 | const DumbNavigate = (state: State, ev) => { 64 | ev.preventDefault(); 65 | return [state, navigate({ to: href, delay: options.navigationDelay })]; 66 | }; 67 | return h( 68 | "a", 69 | { 70 | href, 71 | onclick: PreventDefault, 72 | [navigateEventName]: DumbNavigate, 73 | "aria-current": active, 74 | ...rest, 75 | }, 76 | [...children].map(renderChildren) 77 | ); 78 | } 79 | 80 | // @ts-expect-error 81 | if (window.registerPath) { 82 | // @ts-expect-error 83 | window.registerPath(path); 84 | } 85 | 86 | const RequestNavigation = (state: State, ev) => { 87 | ev.preventDefault(); 88 | const action = PreloadPage(state, href); 89 | if (Array.isArray(action)) { 90 | action.push(navigate({ to: href, delay: options.navigationDelay })); 91 | return action; 92 | } 93 | return [action, navigate({ to: href, delay: options.navigationDelay })]; 94 | }; 95 | 96 | const PreloadPageHandler = (state: State, _ev) => { 97 | return PreloadPage(state, href); 98 | }; 99 | 100 | return h( 101 | "a", 102 | { 103 | href, 104 | onclick: PreventDefault, 105 | [navigateEventName]: RequestNavigation, 106 | onmouseover: PreloadPageHandler, 107 | onfocus: PreloadPageHandler, 108 | "data-path": path, // Used by intersection observer 109 | "data-status": status, 110 | "aria-current": active, 111 | ...rest, 112 | }, 113 | [...children].map(renderChildren) 114 | ); 115 | }; 116 | 117 | export default Link; 118 | -------------------------------------------------------------------------------- /src/components/Router.ts: -------------------------------------------------------------------------------- 1 | import { h, text } from 'hyperapp' 2 | import htmlToVdom from '../utils/htmlToVdom'; 3 | import { ViewContext } from '../types'; 4 | import Link from './Link'; 5 | 6 | /** 7 | * Router component to import in user code. 8 | * 9 | * Renders the correct view depending on the location state 10 | * 11 | */ 12 | const Router = () => ({ state, meta, options }: ViewContext) => { 13 | const { route, path } = state.location 14 | 15 | // 404 Page 16 | if (!route) { 17 | // Display custom 404 page if specified 18 | if (options.notFound && typeof options.notFound === 'function') { 19 | return ( 20 | h('div', { id: 'router-outlet' }, [ 21 | options.notFound(state) 22 | ]) 23 | ) 24 | } 25 | 26 | // Default 404 27 | return ( 28 | h('div', { id: 'router-outlet' }, [ 29 | h('div', { style: { padding: '1rem', textAlign: 'center' } }, [ 30 | h('h1', {}, text('404.')), 31 | h('p', {}, text('Page not found.')), 32 | Link({ href: '/' }, 'Home page') as any 33 | ]) 34 | ]) 35 | ) 36 | } 37 | 38 | const view = meta[route]?.bundle?.default; 39 | 40 | if (view) { 41 | if (state.paths[path] === 'ready') { 42 | 43 | // @ts-expect-error 44 | if (window.pathRendered) { 45 | 46 | // Wait after render 47 | setTimeout(() => { 48 | requestAnimationFrame(() => { 49 | // @ts-expect-error 50 | window.pathRendered(path) 51 | }) 52 | }); 53 | } 54 | 55 | return ( 56 | h('div', { id: 'router-outlet' }, [ 57 | view(state) 58 | ]) 59 | ) 60 | } 61 | } 62 | 63 | // Check if a prerendered piece of HTML can be reused while JS / JSON loads 64 | const previousOutlet = document.getElementById('router-outlet') 65 | if (previousOutlet) { 66 | const node = htmlToVdom(previousOutlet.innerHTML) 67 | return h('div', { id: 'router-outlet' }, node); 68 | } 69 | 70 | // Display custom loader if specified 71 | if (options.loader && typeof options.loader === 'function') { 72 | return ( 73 | h('div', { id: 'router-outlet' }, [ 74 | options.loader(state) 75 | ]) 76 | ) 77 | } 78 | 79 | 80 | // Default loader 81 | return ( 82 | h('div', { id: 'router-outlet' }, [ 83 | h('div', { style: { padding: '1rem', textAlign: 'center' } }, [ 84 | h('h2', {}, text('Loading...')) 85 | ]) 86 | ]) 87 | ) 88 | } 89 | 90 | export default Router 91 | -------------------------------------------------------------------------------- /src/effects/loadRouteBundle.ts: -------------------------------------------------------------------------------- 1 | import InitializePath from '../actions/InitializePath'; 2 | import SetPathStatus from '../actions/SetPathStatus'; 3 | import { LocationState } from '../types'; 4 | 5 | interface LoadRouteBundleArgs { 6 | route: string; 7 | path: string; 8 | meta: any; 9 | location: LocationState; 10 | } 11 | 12 | /** 13 | * Load the JS bundle for a route, then initialize the path 14 | */ 15 | const loadRouteBundleRunner = async (dispatch, { meta, location }: LoadRouteBundleArgs) => { 16 | const { route, path } = location 17 | try { 18 | if (!meta[route]) { 19 | // 404 20 | dispatch(SetPathStatus, { path, status: 'error' }) 21 | } else if (!meta[route].bundle) { 22 | const bundle = await meta[route].promise; 23 | meta[route].bundle = bundle 24 | dispatch(InitializePath, { location, bundle }) 25 | } 26 | } catch (err) { 27 | dispatch(SetPathStatus, { path, status: 'error' }) 28 | throw err 29 | } 30 | } 31 | 32 | const loadRouteBundle = (args: LoadRouteBundleArgs) => [loadRouteBundleRunner, args] 33 | 34 | export default loadRouteBundle 35 | -------------------------------------------------------------------------------- /src/effects/loadStatic.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'hyperapp'; 2 | import SetPathStatus from '../actions/SetPathStatus'; 3 | import { State } from '../types'; 4 | 5 | interface LoadStaticArgs { 6 | loader: () => any | Promise; 7 | action: Action; 8 | error: Action; 9 | } 10 | 11 | /** 12 | * Effect runner for the loadStatic effect 13 | * 14 | * The loadStatic effect, at build time, will cache the data returned from the `loader` promise 15 | * and save it as a JSON file in the build files. 16 | * 17 | * At runtime, it will fetch the pre-saved JSON instead of running the promise 18 | */ 19 | const loadStaticRunner = async (dispatch, { path, loader, action, error }: LoadStaticArgs & { path: string }) => { 20 | try { 21 | 22 | // @ts-ignore 23 | const cachedUrl = window?.HYPERSTATIC_DATA?.cache[path] 24 | 25 | const promise = cachedUrl 26 | ? fetch(cachedUrl).then(res => res.json()) 27 | : loader() 28 | 29 | const data = await promise; 30 | 31 | // @ts-expect-error 32 | if (window.cacheData) { 33 | // @ts-expect-error 34 | window.cacheData(path, data) 35 | } 36 | 37 | dispatch((state: State) => action(SetPathStatus(state, { path, status: 'ready' }), data)) 38 | } catch (err) { 39 | dispatch(error, err) 40 | throw err 41 | } 42 | } 43 | 44 | loadStaticRunner.fxName = 'loadStatic'; 45 | 46 | const loadStatic = (args: LoadStaticArgs) => [loadStaticRunner, args] 47 | 48 | export default loadStatic 49 | -------------------------------------------------------------------------------- /src/effects/navigate.ts: -------------------------------------------------------------------------------- 1 | interface NavigateArgs { 2 | to: string; 3 | delay?: number; 4 | } 5 | 6 | /** 7 | * Trigger a page navigation 8 | */ 9 | const navigateRunner = (dispatch, { to, delay = 0 }: NavigateArgs) => { 10 | dispatchEvent(new CustomEvent("navigationstart")) 11 | setTimeout(() => { 12 | // Internal links 13 | if (to.startsWith('/')) { 14 | history.pushState(null, '', to) 15 | dispatchEvent(new CustomEvent("pushstate")) 16 | } else { 17 | 18 | // Handle external links 19 | window.location.href = to 20 | } 21 | }, delay) 22 | } 23 | 24 | const navigate = (args: NavigateArgs) => [navigateRunner, args] 25 | 26 | export default navigate 27 | -------------------------------------------------------------------------------- /src/hyperstatic.ts: -------------------------------------------------------------------------------- 1 | import { app, h } from 'hyperapp' 2 | import { match } from "path-to-regexp"; 3 | import InitializePath from './actions/InitializePath'; 4 | import SetPathStatus from './actions/SetPathStatus'; 5 | import loadRouteBundle from './effects/loadRouteBundle'; 6 | import onLinkEnteredViewPort from './subscriptions/onLinkEnteredViewPort'; 7 | import onRouteChanged from './subscriptions/onRouteChanged'; 8 | import parseQueryString from './utils/parseQueryString'; 9 | import provide from './utils/provide' 10 | import { Config, LocationState, Options, State } from './types'; 11 | 12 | 13 | 14 | const hyperstatic = ({ routes, options: userOptions, init, view, subscriptions = (_s) => [], ...rest }: Config) => { 15 | 16 | const options: Options = { 17 | eagerLoad: true, 18 | ...userOptions 19 | } 20 | 21 | // Internal values saved for each routes 22 | const meta = Object.keys(routes).reduce((obj, route) => { 23 | obj[route] = { 24 | matcher: match(route), 25 | promise: routes[route], 26 | bundle: null 27 | } 28 | return obj 29 | }, {}) 30 | 31 | // Utility function to parse data from paths 32 | const getLocation = (pathname: string): LocationState => { 33 | const [path, qs] = pathname.split('?') 34 | let matchedRoute; 35 | let params = {}; 36 | for (const route of Object.keys(routes)) { 37 | const maybeMatch = meta[route].matcher(path) 38 | if (maybeMatch) { 39 | matchedRoute = route; 40 | params = maybeMatch.params; 41 | break 42 | } 43 | } 44 | return { 45 | route: matchedRoute, 46 | path, 47 | params, 48 | query: qs ? parseQueryString(qs) : {}, 49 | } 50 | } 51 | 52 | // Preload page Action 53 | const PreloadPage = (state: State, href: string) => { 54 | const location = getLocation(href) 55 | const { route, path } = location 56 | 57 | // If invalid path (404) 58 | if (!route) { 59 | return SetPathStatus(state, { path, status: 'error' }) 60 | } 61 | 62 | const bundle = meta[route]?.bundle; 63 | 64 | // If target route's bundle isn't loaded, load it 65 | if (!bundle) { 66 | return [ 67 | SetPathStatus(state, { path, status: 'loading' }), 68 | loadRouteBundle({ route, path, meta, location }) 69 | ] 70 | } 71 | 72 | return InitializePath(state, { location, bundle }) 73 | } 74 | 75 | // Location changed action 76 | const LocationChanged = ({ location: _, ...state }: State, pathname: string) => { 77 | const location = getLocation(pathname) 78 | const nextState = { location, ...state } 79 | return PreloadPage(nextState, pathname) 80 | } 81 | 82 | const initialPath = window.location.pathname + window.location.search; 83 | 84 | let initialState = Array.isArray(init) ? init[0] : init; 85 | 86 | initialState = { 87 | ...initialState, 88 | paths: {}, 89 | } as State; 90 | 91 | let initAction = LocationChanged(initialState, initialPath); 92 | 93 | if (Array.isArray(init)) { 94 | const effects = init.slice(1) 95 | initAction = Array.isArray(initAction) ? initAction.concat(effects) : [initAction, ...effects] 96 | } 97 | 98 | return app({ 99 | ...rest, 100 | init: initAction, 101 | view: (state) => provide( 102 | { state, meta, options, getLocation, PreloadPage }, 103 | h('div', { id: 'hyperstatic' }, view(state)) 104 | ), 105 | subscriptions: (state) => [ 106 | ...subscriptions(state), 107 | onRouteChanged({ 108 | action: LocationChanged 109 | }), 110 | options.eagerLoad && onLinkEnteredViewPort({ 111 | selector: 'a[data-status=iddle]', 112 | action: PreloadPage 113 | }) 114 | ], 115 | node: document.getElementById('hyperstatic'), 116 | }) 117 | } 118 | 119 | export default hyperstatic 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as hyperstatic } from './hyperstatic' 2 | export { default as Link } from './components/Link' 3 | export { default as Router } from './components/Router' 4 | export { default as navigate } from './effects/navigate' 5 | export { default as loadStatic } from './effects/loadStatic' 6 | export { default as onRouteChangeStart } from './subscriptions/onRouteChangeStart' 7 | export { default as onRouteChanged } from './subscriptions/onRouteChanged' 8 | export { default as htmlToVdom } from './utils/htmlToVdom' 9 | export { default as isPrerendering } from './utils/isPrerendering' 10 | export { 11 | Options, 12 | Config, 13 | LocationState, 14 | PathStatus, 15 | State, 16 | ViewContext, 17 | PathInfo 18 | } from './types' 19 | -------------------------------------------------------------------------------- /src/renderPages.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | import fse from 'fs-extra' 3 | import path from 'path' 4 | import crypto from 'crypto' 5 | import { replaceInFile } from 'replace-in-file' 6 | import handler from 'serve-handler' 7 | import http from 'http' 8 | import events from 'events' 9 | import yargs from 'yargs' 10 | 11 | const createStaticServer = (port, distFolder) => 12 | http.createServer((request, response) => 13 | handler(request, response, { 14 | public: distFolder, 15 | rewrites: [ 16 | { source: '**/*', 'destination': '/index.html' } 17 | ] 18 | }) 19 | ).listen(port) 20 | 21 | const renderPages = async () => { 22 | 23 | const argv = yargs(process.argv.slice(2)) 24 | .usage('Usage: $0 [options]') 25 | .describe('port', 'Port to use for the prerendering server') 26 | .alias('p', 'port') 27 | .describe('dist', 'Directory containing built static files from the bundler') 28 | .alias('d', 'dist') 29 | .describe('entry', 'Entry page to start crawling the site from') 30 | .alias('e', 'entry') 31 | .describe('extra', 'Comma separated list of paths to render if they aren\'t automatically crawled') 32 | .alias('x', 'extra') 33 | .argv; 34 | 35 | // Get command line arguments with defaults 36 | const port = argv.port ? Number(argv.port) : 54321 37 | const distFolder = argv.dist ? String(argv.dist) : 'dist' 38 | const entryPoint = argv.entry ? String(argv.entry) : '/' 39 | const extraPages = typeof argv.extra === 'string' ? argv.extra.split(',') : [] 40 | 41 | try { 42 | 43 | // Spin up a static server to use for prerendering with pupeteer 44 | await createStaticServer(port, distFolder) 45 | 46 | console.log('Rendering site...') 47 | 48 | const baseUrl = `http://localhost:${port}` 49 | 50 | const cache = {} 51 | 52 | const renderEvents = new events.EventEmitter(); 53 | 54 | // Initial render queue 55 | const renderQueue = [ 56 | entryPoint, 57 | ...extraPages 58 | ] 59 | 60 | const browser = await puppeteer.launch({ headless: true }); 61 | const page = await browser.newPage(); 62 | 63 | await page.setUserAgent('puppeteer'); 64 | 65 | await page.exposeFunction('registerPath', (path: string) => { 66 | if (!renderQueue.includes(path)) { 67 | console.log(`Found path ${path}, adding to render queue`); 68 | renderQueue.push(path); 69 | } 70 | }); 71 | 72 | await page.exposeFunction('cacheData', (key: string, data: any) => { 73 | cache[key] = data; 74 | }); 75 | 76 | await page.exposeFunction('pathRendered', (path: string) => { 77 | renderEvents.emit(`${path}-rendered`); 78 | }); 79 | 80 | // Load page 81 | await page.goto(`${baseUrl}${entryPoint}`, { waitUntil: 'networkidle0' }); 82 | 83 | // Pre-render loop 84 | for (let i = 0; i < renderQueue.length; i++) { 85 | const pagePath = renderQueue[i] 86 | 87 | console.log(`Rendering page: ${pagePath} ...`) 88 | 89 | page.on('pageerror', function (err) { 90 | console.log(`Runtime error in page: "${pagePath}" - Error: ${err.toString()}`) 91 | console.error(err) 92 | process.exit(1) 93 | }) 94 | 95 | const pathRendered = new Promise((resolve) => { 96 | renderEvents.once(`${pagePath}-rendered`, resolve) 97 | }) 98 | 99 | // Navigate to the page client-side 100 | await page.evaluate((path) => { 101 | window.history.pushState(null, '', path) 102 | window.dispatchEvent(new CustomEvent("pushstate")) 103 | }, pagePath) 104 | 105 | // Wait for page to render 106 | await pathRendered; 107 | 108 | // Get HTML string from page DOM. 109 | const html = await page.content(); 110 | 111 | // Convert URI path to absolute disk path in the output dir 112 | const pageFilePath = pagePath === '/' ? '/index.html' : `${pagePath}/index.html` 113 | const pageFileAbsolutePath = path.join(process.cwd(), distFolder, pageFilePath) 114 | 115 | // Remove basepath from rendered HTML 116 | // Ex: `, 161 | countMatches: true 162 | }) 163 | console.log(`${results.length} pages updated!`) 164 | 165 | } catch (error) { 166 | console.error(error) 167 | process.exit(1) 168 | } 169 | 170 | console.log('Rendering complete! 🎉') 171 | 172 | process.exit(0) 173 | } 174 | 175 | export default renderPages 176 | -------------------------------------------------------------------------------- /src/subscriptions/onLinkEnteredViewPort.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'hyperapp'; 2 | 3 | let observer = new IntersectionObserver( 4 | (entries, observer) => { 5 | entries.forEach(entry => { 6 | if (entry.isIntersecting) { 7 | // @ts-expect-error 8 | const event = new CustomEvent('linkenteredviewport', { detail: entry.target.dataset.path }); 9 | dispatchEvent(event) 10 | observer.unobserve(entry.target); 11 | } 12 | }); 13 | }, 14 | { 15 | threshold: 0.5 16 | } 17 | ); 18 | 19 | const subRunner = (dispatch, action) => { 20 | const handleLinkEnteredViewport = (ev) => { 21 | dispatch(action, ev.detail) 22 | } 23 | addEventListener('linkenteredviewport', handleLinkEnteredViewport) 24 | return () => { 25 | removeEventListener('linkenteredviewport', handleLinkEnteredViewport) 26 | } 27 | } 28 | 29 | interface OnLinkEnteredViewPortArgs { 30 | selector: string; 31 | action: Action; 32 | } 33 | 34 | /** 35 | * Every time a "Link" component enters the viewport, 36 | * trigger the given action with the link's path as params 37 | */ 38 | const onLinkEnteredViewPort = ({ 39 | selector, 40 | action 41 | }: OnLinkEnteredViewPortArgs) => { 42 | 43 | // After each render 44 | setTimeout(() => { 45 | requestAnimationFrame(() => { 46 | 47 | // TODO: research if having the same element observed many times is an issue and how to avoid this 48 | document.querySelectorAll(selector).forEach(link => { 49 | observer.observe(link) 50 | }); 51 | }) 52 | }); 53 | 54 | return [ 55 | subRunner, 56 | action 57 | ] 58 | } 59 | 60 | 61 | export default onLinkEnteredViewPort 62 | -------------------------------------------------------------------------------- /src/subscriptions/onRouteChangeStart.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'hyperapp' 2 | 3 | interface OnRouteChangeStartArgs { 4 | action: Action; 5 | } 6 | 7 | /** 8 | * Every time the browser's location changes, 9 | * trigger the given action with the new location as params 10 | */ 11 | const onRouteChangeStartRunner = (dispatch, { action }: OnRouteChangeStartArgs) => { 12 | const handleLocationChange = () => { 13 | dispatch(action) 14 | } 15 | addEventListener('navigationstart', handleLocationChange) 16 | return () => { 17 | removeEventListener('navigationstart', handleLocationChange) 18 | } 19 | } 20 | 21 | const onRouteChangeStart = (args: OnRouteChangeStartArgs) => [onRouteChangeStartRunner, args] 22 | 23 | export default onRouteChangeStart 24 | -------------------------------------------------------------------------------- /src/subscriptions/onRouteChanged.ts: -------------------------------------------------------------------------------- 1 | import { Action } from 'hyperapp' 2 | 3 | interface OnRouteChangedArgs { 4 | action: Action; 5 | } 6 | 7 | /** 8 | * Every time the browser's location changes, 9 | * trigger the given action with the new location as params 10 | */ 11 | const onRouteChangedRunner = (dispatch, { action }: OnRouteChangedArgs) => { 12 | const handleLocationChange = () => { 13 | dispatch(action, window.location.pathname + window.location.search) 14 | } 15 | addEventListener('pushstate', handleLocationChange) 16 | addEventListener('popstate', handleLocationChange) 17 | return () => { 18 | removeEventListener('pushstate', handleLocationChange) 19 | removeEventListener('popstate', handleLocationChange) 20 | } 21 | } 22 | 23 | const onRouteChanged = (args: OnRouteChangedArgs) => [onRouteChangedRunner, args] 24 | 25 | export default onRouteChanged 26 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | type Action = (state: State, args: T) => State | Array 2 | 3 | export interface Options { 4 | baseUrl?: string; 5 | loader?: (state: State) => any 6 | notFound?: (state: State) => any 7 | fastClicks?: boolean 8 | eagerLoad?: boolean 9 | navigationDelay?: number 10 | } 11 | 12 | export interface Config { 13 | routes: Record | any>; 14 | options?: Options; 15 | init: Record; 16 | view: (state: State) => any; 17 | subscriptions?: (state: State) => any[]; 18 | } 19 | 20 | export interface LocationState { 21 | route?: string; 22 | path: string; 23 | params: any; 24 | query: any; 25 | } 26 | 27 | export type PathStatus = 'iddle' | 'loading' | 'fetching' | 'ready' | 'error'; 28 | 29 | export interface State { 30 | location: LocationState; 31 | paths: Record; 32 | [x: string]: any; 33 | } 34 | 35 | export interface ViewContext { 36 | state: State 37 | options: Options 38 | meta: any 39 | getLocation: (path: string) => LocationState 40 | PreloadPage: Action 41 | LocationChanged: Action 42 | } 43 | 44 | export interface PathInfo extends LocationState { 45 | status: PathStatus; 46 | active: boolean; 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/htmlToVdom.ts: -------------------------------------------------------------------------------- 1 | import { h, text } from 'hyperapp' 2 | 3 | /** 4 | * Html to hyperapp VDOM converter 5 | * Someone should make this a package... 6 | */ 7 | 8 | const mapProps = attrs => ( 9 | [...attrs].reduce( 10 | (props, attr) => ( 11 | attr.nodeName === 'style' 12 | ? props // ignore string style definitions for now. 13 | : { ...props, [attr.nodeName]: attr.nodeValue } 14 | ), 15 | {} 16 | ) 17 | ) 18 | 19 | const mapChildren = (childNodes) => { 20 | if (!!childNodes && childNodes.length > 0) { 21 | return [...childNodes].map(node => mapVNode(node)) 22 | } else { 23 | return [] 24 | } 25 | } 26 | 27 | const mapVNode = (node) => { 28 | switch (node.nodeType) { 29 | case Node.TEXT_NODE: 30 | return text(node.nodeValue) 31 | case Node.ELEMENT_NODE: 32 | return h(node.tagName, mapProps(node.attributes), mapChildren(node.childNodes)) 33 | default: 34 | throw new Error(`${node.nodeType} is not supported`) 35 | } 36 | } 37 | 38 | const htmlToVdom = (html) => { 39 | const parser = new DOMParser() 40 | const doc = parser.parseFromString(html, 'text/html') 41 | const node = mapVNode(doc.body) 42 | return node.children 43 | } 44 | 45 | export default htmlToVdom 46 | -------------------------------------------------------------------------------- /src/utils/isPrerendering.ts: -------------------------------------------------------------------------------- 1 | const isPrerendering = () => window.navigator.userAgent === 'puppeteer'; 2 | 3 | export default isPrerendering 4 | -------------------------------------------------------------------------------- /src/utils/parseQueryString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Turn a query string into an object 3 | * 4 | * Ex: ?a=2&b=3 becomes: 5 | * 6 | * { 7 | * a: 2, 8 | * b: 3 9 | * } 10 | * 11 | */ 12 | const parseQueryString = (qs: string) => { 13 | return Object.fromEntries(new URLSearchParams(qs)) 14 | } 15 | 16 | export default parseQueryString 17 | -------------------------------------------------------------------------------- /src/utils/provide.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Recursively walk the vnode tree, if a function is found, 3 | * call it with the given value and repeat 4 | */ 5 | const provide = (value, node) => 6 | !node 7 | ? node 8 | : Array.isArray(node) 9 | ? node.map((n) => provide(value, n)).flat() 10 | : typeof node === 'function' 11 | ? provide(value, node(value)) 12 | : node.children 13 | ? { ...node, children: provide(value, node.children) } 14 | : node 15 | 16 | export default provide 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "declaration": true, 8 | "outDir": "dist", 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": ["node_modules", "dist"] 14 | } 15 | --------------------------------------------------------------------------------