| author | |
| committer | |
| log | 9649288a27c2bda9361572d08e45438ea89a789f |
| tree | e46aa60044c6a8d67a4e60b25b6c012ac27738a9 |
| parent | 24465d155378c4b617a658a27385f1da2e381d90 |
| signature | Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs |
11 files changed, 631 insertions(+), 77 deletions(-)
package-lock.json+153-22| ... | @@ -4,6 +4,7 @@ | ... | @@ -4,6 +4,7 @@ |
| 4 | "requires": true, | 4 | "requires": true, |
| 5 | "packages": { | 5 | "packages": { |
| 6 | "": { | 6 | "": { |
| 7 | "name": "sitegen", | ||
| 7 | "dependencies": { | 8 | "dependencies": { |
| 8 | "@hono/node-server": "1.14.3", | 9 | "@hono/node-server": "1.14.3", |
| 9 | "@mdx-js/mdx": "3.1.0", | 10 | "@mdx-js/mdx": "3.1.0", |
| ... | @@ -58,6 +59,7 @@ | ... | @@ -58,6 +59,7 @@ |
| 58 | "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", | 59 | "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", |
| 59 | "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", | 60 | "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", |
| 60 | "license": "MIT", | 61 | "license": "MIT", |
| 62 | "peer": true, | ||
| 61 | "dependencies": { | 63 | "dependencies": { |
| 62 | "@babel/code-frame": "^7.27.1", | 64 | "@babel/code-frame": "^7.27.1", |
| 63 | "@babel/generator": "^7.28.3", | 65 | "@babel/generator": "^7.28.3", |
| ... | @@ -1168,11 +1170,129 @@ | ... | @@ -1168,11 +1170,129 @@ |
| 1168 | "node": ">=10" | 1170 | "node": ">=10" |
| 1169 | } | 1171 | } |
| 1170 | }, | 1172 | }, |
| 1171 | "node_modules/@svta/common-media-library": { | 1173 | "node_modules/@svta/cml-608": { |
| 1172 | "version": "0.12.4", | 1174 | "version": "1.0.1", |
| 1173 | "resolved": "https://registry.npmjs.org/@svta/common-media-library/-/common-media-library-0.12.4.tgz", | 1175 | "resolved": "https://registry.npmjs.org/@svta/cml-608/-/cml-608-1.0.1.tgz", |
| 1174 | "integrity": "sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==", | 1176 | "integrity": "sha512-Y/Ier9VPUSOBnf0bJqdDyTlPrt4dDB+jk5mYHa1bnD2kcRl8qn7KkW3PRuj4w1aVN+BS2eHmsLxodt7P2hylUg==", |
| 1175 | "license": "Apache-2.0" | 1177 | "license": "Apache-2.0", |
| 1178 | "engines": { | ||
| 1179 | "node": ">=20" | ||
| 1180 | } | ||
| 1181 | }, | ||
| 1182 | "node_modules/@svta/cml-cmcd": { | ||
| 1183 | "version": "1.0.1", | ||
| 1184 | "resolved": "https://registry.npmjs.org/@svta/cml-cmcd/-/cml-cmcd-1.0.1.tgz", | ||
| 1185 | "integrity": "sha512-eox305g+QUJgXqOLVrbgxeQHCgl90ewwQ9O2bIoo7m+hanR8Xswu5CknFnT5qqIbLOHfw80ug+raycoAFHTQ+w==", | ||
| 1186 | "license": "Apache-2.0", | ||
| 1187 | "engines": { | ||
| 1188 | "node": ">=20" | ||
| 1189 | }, | ||
| 1190 | "peerDependencies": { | ||
| 1191 | "@svta/cml-cta": "1.0.1", | ||
| 1192 | "@svta/cml-structured-field-values": "1.0.1", | ||
| 1193 | "@svta/cml-utils": "1.0.1" | ||
| 1194 | } | ||
| 1195 | }, | ||
| 1196 | "node_modules/@svta/cml-cmsd": { | ||
| 1197 | "version": "1.0.1", | ||
| 1198 | "resolved": "https://registry.npmjs.org/@svta/cml-cmsd/-/cml-cmsd-1.0.1.tgz", | ||
| 1199 | "integrity": "sha512-+nIB8PuSfb/qw+xGaArPhNqPm84tBJUbe3H1DnPL5QUsjSUI7mUIUQwAtRV1ZdEu0+80g9i0op79woB0OIwr/g==", | ||
| 1200 | "license": "Apache-2.0", | ||
| 1201 | "engines": { | ||
| 1202 | "node": ">=20" | ||
| 1203 | }, | ||
| 1204 | "peerDependencies": { | ||
| 1205 | "@svta/cml-cta": "1.0.1", | ||
| 1206 | "@svta/cml-structured-field-values": "1.0.1", | ||
| 1207 | "@svta/cml-utils": "1.0.1" | ||
| 1208 | } | ||
| 1209 | }, | ||
| 1210 | "node_modules/@svta/cml-cta": { | ||
| 1211 | "version": "1.0.1", | ||
| 1212 | "resolved": "https://registry.npmjs.org/@svta/cml-cta/-/cml-cta-1.0.1.tgz", | ||
| 1213 | "integrity": "sha512-jcXqNIPv26bmFxIOFh8/c3+6WLH4qBjKpq9qTQcggDPoHuV1YBydMsJLOnYPDeK8rNMKcAkFLbnDRvyJthu5yw==", | ||
| 1214 | "license": "Apache-2.0", | ||
| 1215 | "peer": true, | ||
| 1216 | "engines": { | ||
| 1217 | "node": ">=20" | ||
| 1218 | }, | ||
| 1219 | "peerDependencies": { | ||
| 1220 | "@svta/cml-structured-field-values": "1.0.1", | ||
| 1221 | "@svta/cml-utils": "1.0.1" | ||
| 1222 | } | ||
| 1223 | }, | ||
| 1224 | "node_modules/@svta/cml-dash": { | ||
| 1225 | "version": "1.0.1", | ||
| 1226 | "resolved": "https://registry.npmjs.org/@svta/cml-dash/-/cml-dash-1.0.1.tgz", | ||
| 1227 | "integrity": "sha512-lYnD1I7FUbbQND+xICI+kcRaRXuT+whKk27R8m8me5VMVu2sMsAMc7Yui6l9sxw2cBKt8pSETPYRm/1+n4LZkw==", | ||
| 1228 | "license": "Apache-2.0", | ||
| 1229 | "engines": { | ||
| 1230 | "node": ">=20" | ||
| 1231 | }, | ||
| 1232 | "peerDependencies": { | ||
| 1233 | "@svta/cml-utils": "1.0.1" | ||
| 1234 | } | ||
| 1235 | }, | ||
| 1236 | "node_modules/@svta/cml-id3": { | ||
| 1237 | "version": "1.0.1", | ||
| 1238 | "resolved": "https://registry.npmjs.org/@svta/cml-id3/-/cml-id3-1.0.1.tgz", | ||
| 1239 | "integrity": "sha512-90fGlL1qRI88CcaB89k6NG6cC3kky4Eu2jwqU4HefqK+S5k2OASUxf8JXkGz+DsdaiY7sh51vGPYdolfBZS7ug==", | ||
| 1240 | "license": "Apache-2.0", | ||
| 1241 | "engines": { | ||
| 1242 | "node": ">=20" | ||
| 1243 | }, | ||
| 1244 | "peerDependencies": { | ||
| 1245 | "@svta/cml-utils": "1.0.1" | ||
| 1246 | } | ||
| 1247 | }, | ||
| 1248 | "node_modules/@svta/cml-request": { | ||
| 1249 | "version": "1.0.1", | ||
| 1250 | "resolved": "https://registry.npmjs.org/@svta/cml-request/-/cml-request-1.0.1.tgz", | ||
| 1251 | "integrity": "sha512-enL19BuXUjFkDDDF9jdNwUclMNPRsagnjGAetVC7xcmpDMpEx+ZLgsDip6BFNg5p6izSEk/OyujTWW1r8bDNiA==", | ||
| 1252 | "license": "Apache-2.0", | ||
| 1253 | "engines": { | ||
| 1254 | "node": ">=20" | ||
| 1255 | }, | ||
| 1256 | "peerDependencies": { | ||
| 1257 | "@svta/cml-utils": "1.0.1", | ||
| 1258 | "@svta/cml-xml": "1.0.1" | ||
| 1259 | } | ||
| 1260 | }, | ||
| 1261 | "node_modules/@svta/cml-structured-field-values": { | ||
| 1262 | "version": "1.0.1", | ||
| 1263 | "resolved": "https://registry.npmjs.org/@svta/cml-structured-field-values/-/cml-structured-field-values-1.0.1.tgz", | ||
| 1264 | "integrity": "sha512-Kibciki59Pon3Pn/sl5uyrbJcSpZQDKqdCfDrokBvOdLoqqcd0oFrkEPsZBiuuIODX1CB80612xe8hopeFDyBA==", | ||
| 1265 | "license": "Apache-2.0", | ||
| 1266 | "peer": true, | ||
| 1267 | "engines": { | ||
| 1268 | "node": ">=20" | ||
| 1269 | }, | ||
| 1270 | "peerDependencies": { | ||
| 1271 | "@svta/cml-utils": "1.0.1" | ||
| 1272 | } | ||
| 1273 | }, | ||
| 1274 | "node_modules/@svta/cml-utils": { | ||
| 1275 | "version": "1.0.1", | ||
| 1276 | "resolved": "https://registry.npmjs.org/@svta/cml-utils/-/cml-utils-1.0.1.tgz", | ||
| 1277 | "integrity": "sha512-kso3curTJfp00I1mKFoBliBApjn4aPE+wF8cPucf7TrSDVWZDeLLuF14ASmUE9m7rnrqTTK4878VvmXaXcCCfQ==", | ||
| 1278 | "license": "Apache-2.0", | ||
| 1279 | "peer": true, | ||
| 1280 | "engines": { | ||
| 1281 | "node": ">=20" | ||
| 1282 | } | ||
| 1283 | }, | ||
| 1284 | "node_modules/@svta/cml-xml": { | ||
| 1285 | "version": "1.0.1", | ||
| 1286 | "resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz", | ||
| 1287 | "integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==", | ||
| 1288 | "license": "Apache-2.0", | ||
| 1289 | "peer": true, | ||
| 1290 | "engines": { | ||
| 1291 | "node": ">=20" | ||
| 1292 | }, | ||
| 1293 | "peerDependencies": { | ||
| 1294 | "@svta/cml-utils": "1.0.1" | ||
| 1295 | } | ||
| 1176 | }, | 1296 | }, |
| 1177 | "node_modules/@tootallnate/quickjs-emscripten": { | 1297 | "node_modules/@tootallnate/quickjs-emscripten": { |
| 1178 | "version": "0.23.0", | 1298 | "version": "0.23.0", |
| ... | @@ -1271,6 +1391,7 @@ | ... | @@ -1271,6 +1391,7 @@ |
| 1271 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", | 1391 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", |
| 1272 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", | 1392 | "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", |
| 1273 | "license": "MIT", | 1393 | "license": "MIT", |
| 1394 | "peer": true, | ||
| 1274 | "bin": { | 1395 | "bin": { |
| 1275 | "acorn": "bin/acorn" | 1396 | "acorn": "bin/acorn" |
| 1276 | }, | 1397 | }, |
| ... | @@ -1530,6 +1651,7 @@ | ... | @@ -1530,6 +1651,7 @@ |
| 1530 | } | 1651 | } |
| 1531 | ], | 1652 | ], |
| 1532 | "license": "MIT", | 1653 | "license": "MIT", |
| 1654 | "peer": true, | ||
| 1533 | "dependencies": { | 1655 | "dependencies": { |
| 1534 | "baseline-browser-mapping": "^2.8.9", | 1656 | "baseline-browser-mapping": "^2.8.9", |
| 1535 | "caniuse-lite": "^1.0.30001746", | 1657 | "caniuse-lite": "^1.0.30001746", |
| ... | @@ -1778,12 +1900,18 @@ | ... | @@ -1778,12 +1900,18 @@ |
| 1778 | "license": "MIT" | 1900 | "license": "MIT" |
| 1779 | }, | 1901 | }, |
| 1780 | "node_modules/dashjs": { | 1902 | "node_modules/dashjs": { |
| 1781 | "version": "5.0.3", | 1903 | "version": "5.1.1", |
| 1782 | "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.0.3.tgz", | 1904 | "resolved": "https://registry.npmjs.org/dashjs/-/dashjs-5.1.1.tgz", |
| 1783 | "integrity": "sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==", | 1905 | "integrity": "sha512-BzNXlUgzEjhuZ5M5hlSp1qIyQHZ7NpXAR0loP9DAAFVZj/ntL1DHeZ7qp/L3bvI4rq50X5indkAZQ3zEHWJoCA==", |
| 1784 | "license": "BSD-3-Clause", | 1906 | "license": "BSD-3-Clause", |
| 1785 | "dependencies": { | 1907 | "dependencies": { |
| 1786 | "@svta/common-media-library": "^0.12.4", | 1908 | "@svta/cml-608": "1.0.1", |
| 1909 | "@svta/cml-cmcd": "1.0.1", | ||
| 1910 | "@svta/cml-cmsd": "1.0.1", | ||
| 1911 | "@svta/cml-dash": "1.0.1", | ||
| 1912 | "@svta/cml-id3": "1.0.1", | ||
| 1913 | "@svta/cml-request": "1.0.1", | ||
| 1914 | "@svta/cml-xml": "1.0.1", | ||
| 1787 | "bcp-47-match": "^2.0.3", | 1915 | "bcp-47-match": "^2.0.3", |
| 1788 | "bcp-47-normalize": "^2.3.0", | 1916 | "bcp-47-normalize": "^2.3.0", |
| 1789 | "codem-isoboxer": "0.3.10", | 1917 | "codem-isoboxer": "0.3.10", |
| ... | @@ -1868,9 +1996,9 @@ | ... | @@ -1868,9 +1996,9 @@ |
| 1868 | } | 1996 | } |
| 1869 | }, | 1997 | }, |
| 1870 | "node_modules/devalue": { | 1998 | "node_modules/devalue": { |
| 1871 | "version": "5.3.2", | 1999 | "version": "5.6.2", |
| 1872 | "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.3.2.tgz", | 2000 | "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", |
| 1873 | "integrity": "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw==", | 2001 | "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", |
| 1874 | "license": "MIT" | 2002 | "license": "MIT" |
| 1875 | }, | 2003 | }, |
| 1876 | "node_modules/devlop": { | 2004 | "node_modules/devlop": { |
| ... | @@ -1890,7 +2018,8 @@ | ... | @@ -1890,7 +2018,8 @@ |
| 1890 | "version": "0.0.1508733", | 2018 | "version": "0.0.1508733", |
| 1891 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", | 2019 | "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz", |
| 1892 | "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", | 2020 | "integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==", |
| 1893 | "license": "BSD-3-Clause" | 2021 | "license": "BSD-3-Clause", |
| 2022 | "peer": true | ||
| 1894 | }, | 2023 | }, |
| 1895 | "node_modules/electron-to-chromium": { | 2024 | "node_modules/electron-to-chromium": { |
| 1896 | "version": "1.5.229", | 2025 | "version": "1.5.229", |
| ... | @@ -2392,10 +2521,11 @@ | ... | @@ -2392,10 +2521,11 @@ |
| 2392 | "license": "Apache-2.0" | 2521 | "license": "Apache-2.0" |
| 2393 | }, | 2522 | }, |
| 2394 | "node_modules/hono": { | 2523 | "node_modules/hono": { |
| 2395 | "version": "4.10.3", | 2524 | "version": "4.11.7", |
| 2396 | "resolved": "https://registry.npmjs.org/hono/-/hono-4.10.3.tgz", | 2525 | "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", |
| 2397 | "integrity": "sha512-2LOYWUbnhdxdL8MNbNg9XZig6k+cZXm5IjHn2Aviv7honhBMOHb+jxrKIeJRZJRmn+htUCKhaicxwXuUDlchRA==", | 2526 | "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", |
| 2398 | "license": "MIT", | 2527 | "license": "MIT", |
| 2528 | "peer": true, | ||
| 2399 | "engines": { | 2529 | "engines": { |
| 2400 | "node": ">=16.9.0" | 2530 | "node": ">=16.9.0" |
| 2401 | } | 2531 | } |
| ... | @@ -2581,9 +2711,9 @@ | ... | @@ -2581,9 +2711,9 @@ |
| 2581 | "license": "MIT" | 2711 | "license": "MIT" |
| 2582 | }, | 2712 | }, |
| 2583 | "node_modules/js-yaml": { | 2713 | "node_modules/js-yaml": { |
| 2584 | "version": "4.1.0", | 2714 | "version": "4.1.1", |
| 2585 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", | 2715 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", |
| 2586 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", | 2716 | "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", |
| 2587 | "license": "MIT", | 2717 | "license": "MIT", |
| 2588 | "dependencies": { | 2718 | "dependencies": { |
| 2589 | "argparse": "^2.0.1" | 2719 | "argparse": "^2.0.1" |
| ... | @@ -2847,9 +2977,9 @@ | ... | @@ -2847,9 +2977,9 @@ |
| 2847 | } | 2977 | } |
| 2848 | }, | 2978 | }, |
| 2849 | "node_modules/mdast-util-to-hast": { | 2979 | "node_modules/mdast-util-to-hast": { |
| 2850 | "version": "13.2.0", | 2980 | "version": "13.2.1", |
| 2851 | "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", | 2981 | "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", |
| 2852 | "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", | 2982 | "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", |
| 2853 | "license": "MIT", | 2983 | "license": "MIT", |
| 2854 | "dependencies": { | 2984 | "dependencies": { |
| 2855 | "@types/hast": "^3.0.0", | 2985 | "@types/hast": "^3.0.0", |
| ... | @@ -4225,6 +4355,7 @@ | ... | @@ -4225,6 +4355,7 @@ |
| 4225 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", | 4355 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", |
| 4226 | "devOptional": true, | 4356 | "devOptional": true, |
| 4227 | "license": "Apache-2.0", | 4357 | "license": "Apache-2.0", |
| 4358 | "peer": true, | ||
| 4228 | "bin": { | 4359 | "bin": { |
| 4229 | "tsc": "bin/tsc", | 4360 | "tsc": "bin/tsc", |
| 4230 | "tsserver": "bin/tsserver" | 4361 | "tsserver": "bin/tsserver" |
src/file-viewer/backend.tsx+19-5| ... | @@ -13,11 +13,18 @@ app.get("/file/*", async (c, next) => { | ... | @@ -13,11 +13,18 @@ app.get("/file/*", async (c, next) => { |
| 13 | const ua = c.req.header("User-Agent")?.toLowerCase() ?? ""; | 13 | const ua = c.req.header("User-Agent")?.toLowerCase() ?? ""; |
| 14 | const lofi = ua.includes("msie") || false; | 14 | const lofi = ua.includes("msie") || false; |
| 15 | 15 | ||
| 16 | let rawFilePath = c.req.path.slice(5) || "/"; | ||
| 17 | |||
| 16 | // Some robots ignore 'robots.txt' which violates the license agreement. | 18 | // Some robots ignore 'robots.txt' which violates the license agreement. |
| 17 | if ( | 19 | if ( |
| 18 | (!ua.startsWith("mozilla/")) || | 20 | !rawFilePath.endsWith("thumbnail.jpeg") && ( |
| 19 | ua.includes("headless") || | 21 | (!ua.startsWith("mozilla/") && |
| 20 | ua.includes("discord") | 22 | // these two are for AirPlay |
| 23 | !ua.includes("airplay") && | ||
| 24 | !ua.includes("applecoremedia")) || | ||
| 25 | ua.includes("headless") || | ||
| 26 | ua.includes("discord") | ||
| 27 | ) | ||
| 21 | ) { | 28 | ) { |
| 22 | return c.body( | 29 | return c.body( |
| 23 | "Forbidden\n\nYour user agent, " + JSON.stringify(ua) + | 30 | "Forbidden\n\nYour user agent, " + JSON.stringify(ua) + |
| ... | @@ -28,8 +35,6 @@ app.get("/file/*", async (c, next) => { | ... | @@ -28,8 +35,6 @@ app.get("/file/*", async (c, next) => { |
| 28 | ); | 35 | ); |
| 29 | } | 36 | } |
| 30 | 37 | ||
| 31 | let rawFilePath = c.req.path.slice(5) || "/"; | ||
| 32 | |||
| 33 | let derivedKey = ""; | 38 | let derivedKey = ""; |
| 34 | if (rawFilePath.includes(":/")) { | 39 | if (rawFilePath.includes(":/")) { |
| 35 | [rawFilePath, derivedKey] = rawFilePath.split(":/") as [string, string]; | 40 | [rawFilePath, derivedKey] = rawFilePath.split(":/") as [string, string]; |
| ... | @@ -102,6 +107,14 @@ app.get("/file/*", async (c, next) => { | ... | @@ -102,6 +107,14 @@ app.get("/file/*", async (c, next) => { |
| 102 | return next(); | 107 | return next(); |
| 103 | } | 108 | } |
| 104 | } | 109 | } |
| 110 | |||
| 111 | // If an asset is too big, just redirect to the direct server URL. | ||
| 112 | if (size >= 1_000_000_000 && !derivedKey) { | ||
| 113 | return c.redirect( | ||
| 114 | `https://file.paperclover.net/public${escapeUri(file.path)}`, | ||
| 115 | ); | ||
| 116 | } | ||
| 117 | |||
| 105 | const etag = `"${hash}"`; | 118 | const etag = `"${hash}"`; |
| 106 | 119 | ||
| 107 | const headers = new Headers({ | 120 | const headers = new Headers({ |
| ... | @@ -313,3 +326,4 @@ import { Speedbump } from "@/file-viewer/cotyledon.tsx"; | ... | @@ -313,3 +326,4 @@ import { Speedbump } from "@/file-viewer/cotyledon.tsx"; |
| 313 | import * as cache from "@/file-viewer/cache.ts"; | 326 | import * as cache from "@/file-viewer/cache.ts"; |
| 314 | import * as derived from "@/file-viewer/models/derived.ts"; | 327 | import * as derived from "@/file-viewer/models/derived.ts"; |
| 315 | import { UNWRAP } from "lib/assert.ts"; | 328 | import { UNWRAP } from "lib/assert.ts"; |
| 329 | import { escapeUri } from "./format.ts"; |
src/file-viewer/cache.ts+34-10| ... | @@ -145,6 +145,9 @@ export async function read( | ... | @@ -145,6 +145,9 @@ export async function read( |
| 145 | 145 | ||
| 146 | if (localOnly) return null; | 146 | if (localOnly) return null; |
| 147 | 147 | ||
| 148 | const suffix = derived ? ":/" + derived.split("/").slice(1).join("/") : ""; | ||
| 149 | const url = `${sotUrl}/file${file.path}${suffix}`; | ||
| 150 | |||
| 148 | // Disk cache | 151 | // Disk cache |
| 149 | const diskPath = diskCacheRoot.join(hash); | 152 | const diskPath = diskCacheRoot.join(hash); |
| 150 | let diskFile = diskCache.get(diskPath); | 153 | let diskFile = diskCache.get(diskPath); |
| ... | @@ -158,6 +161,17 @@ export async function read( | ... | @@ -158,6 +161,17 @@ export async function read( |
| 158 | }); | 161 | }); |
| 159 | if (diskStream) { | 162 | if (diskStream) { |
| 160 | const isPending = diskFile.written.value !== diskFile.size; | 163 | const isPending = diskFile.written.value !== diskFile.size; |
| 164 | |||
| 165 | // TODO: fix this? i'm suspicious | ||
| 166 | if (isPending) { | ||
| 167 | diskStream.close(); | ||
| 168 | return { | ||
| 169 | stream: await fetchAndSlice(rel, url, start, end), | ||
| 170 | size, | ||
| 171 | src: "contended", | ||
| 172 | }; | ||
| 173 | } | ||
| 174 | |||
| 161 | console.info(`from ${isPending ? "pending " : ""}disk cache: ${rel}`); | 175 | console.info(`from ${isPending ? "pending " : ""}disk cache: ${rel}`); |
| 162 | return { | 176 | return { |
| 163 | stream: stream.Readable.toWeb(diskStream), | 177 | stream: stream.Readable.toWeb(diskStream), |
| ... | @@ -169,15 +183,11 @@ export async function read( | ... | @@ -169,15 +183,11 @@ export async function read( |
| 169 | } | 183 | } |
| 170 | } | 184 | } |
| 171 | 185 | ||
| 172 | // Fetch the source of truth | ||
| 173 | const suffix = derived ? ":/" + derived.split("/").slice(1).join("/") : ""; | ||
| 174 | const url = `${sotUrl}/file${file.path}${suffix}`; | ||
| 175 | |||
| 176 | // Do not cache large assets | 186 | // Do not cache large assets |
| 177 | if (size > 1_000_000_000) { | 187 | if (size > 1_000_000_000) { |
| 178 | console.info(`skipping disk cache due to large asset: ${rel}`); | 188 | console.info(`skipping disk cache due to large asset: ${rel}`); |
| 179 | return { | 189 | return { |
| 180 | stream: fetchAndSlice(rel, url, start, end), | 190 | stream: await fetchAndSlice(rel, url, start, end), |
| 181 | size, | 191 | size, |
| 182 | src: "ignored", | 192 | src: "ignored", |
| 183 | }; | 193 | }; |
| ... | @@ -186,7 +196,7 @@ export async function read( | ... | @@ -186,7 +196,7 @@ export async function read( |
| 186 | if (diskCache.unlockedCapacity < size) { | 196 | if (diskCache.unlockedCapacity < size) { |
| 187 | console.info(`skipping disk cache due to contention: ${rel}`); | 197 | console.info(`skipping disk cache due to contention: ${rel}`); |
| 188 | return { | 198 | return { |
| 189 | stream: fetchAndSlice(rel, url, start, end), | 199 | stream: await fetchAndSlice(rel, url, start, end), |
| 190 | size, | 200 | size, |
| 191 | src: "contended", | 201 | src: "contended", |
| 192 | }; | 202 | }; |
| ... | @@ -196,7 +206,7 @@ export async function read( | ... | @@ -196,7 +206,7 @@ export async function read( |
| 196 | if (noWrite) { | 206 | if (noWrite) { |
| 197 | console.info(`skipping disk cache due to config: ${rel}`); | 207 | console.info(`skipping disk cache due to config: ${rel}`); |
| 198 | return { | 208 | return { |
| 199 | stream: fetchAndSlice(rel, url, start, end), | 209 | stream: await fetchAndSlice(rel, url, start, end), |
| 200 | size, | 210 | size, |
| 201 | src: "ratelimit", | 211 | src: "ratelimit", |
| 202 | }; | 212 | }; |
| ... | @@ -207,7 +217,16 @@ export async function read( | ... | @@ -207,7 +217,16 @@ export async function read( |
| 207 | diskFile = { size, written: new async.Watch(0), memory: new Map() }; | 217 | diskFile = { size, written: new async.Watch(0), memory: new Map() }; |
| 208 | diskCache.set(diskPath, diskFile); | 218 | diskCache.set(diskPath, diskFile); |
| 209 | const lock = diskCache.lock(diskPath); | 219 | const lock = diskCache.lock(diskPath); |
| 210 | return fetchAndStore(rel, url, size, diskPath, diskFile, start, end, lock); | 220 | return await fetchAndStore( |
| 221 | rel, | ||
| 222 | url, | ||
| 223 | size, | ||
| 224 | diskPath, | ||
| 225 | diskFile, | ||
| 226 | start, | ||
| 227 | end, | ||
| 228 | lock, | ||
| 229 | ); | ||
| 211 | } | 230 | } |
| 212 | 231 | ||
| 213 | async function fetchAndStore( | 232 | async function fetchAndStore( |
| ... | @@ -218,7 +237,7 @@ async function fetchAndStore( | ... | @@ -218,7 +237,7 @@ async function fetchAndStore( |
| 218 | diskFile: DiskFile, | 237 | diskFile: DiskFile, |
| 219 | start: number, | 238 | start: number, |
| 220 | end: number, | 239 | end: number, |
| 221 | lock: Lock, | 240 | lock: ts.Dispose, |
| 222 | ) { | 241 | ) { |
| 223 | try { | 242 | try { |
| 224 | let response = await fetch(rel, url); | 243 | let response = await fetch(rel, url); |
| ... | @@ -236,7 +255,11 @@ async function fetchAndStore( | ... | @@ -236,7 +255,11 @@ async function fetchAndStore( |
| 236 | diskFile.written.value = Infinity; | 255 | diskFile.written.value = Infinity; |
| 237 | }); | 256 | }); |
| 238 | 257 | ||
| 239 | return { stream: sliceStream(response, start, end), size, src: "miss" }; | 258 | return { |
| 259 | stream: sliceStream(response, start, end), | ||
| 260 | size, | ||
| 261 | src: "miss", | ||
| 262 | } as const; | ||
| 240 | } catch (e) { | 263 | } catch (e) { |
| 241 | lock(); | 264 | lock(); |
| 242 | throw e; | 265 | throw e; |
| ... | @@ -590,3 +613,4 @@ import type { ReadableStream } from "node:stream/web"; | ... | @@ -590,3 +613,4 @@ import type { ReadableStream } from "node:stream/web"; |
| 590 | import { Lru } from "lib/Lru.ts"; | 613 | import { Lru } from "lib/Lru.ts"; |
| 591 | import { Path } from "#sitegen/path"; | 614 | import { Path } from "#sitegen/path"; |
| 592 | import { ASSERT, UNWRAP } from "lib/assert.ts"; | 615 | import { ASSERT, UNWRAP } from "lib/assert.ts"; |
| 616 | import * as ts from "lib/ts.ts"; |
src/pages/history-of-japan-reanimated.client.ts created+137| ... | @@ -0,0 +1,137 @@ | ||
| 1 | // this script is mostly written by ai | ||
| 2 | |||
| 3 | const urls: Record<string, string> = { | ||
| 4 | "Kappacap": "https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg", | ||
| 5 | "atamai": "https://www.tumblr.com/atamai-m8", | ||
| 6 | "moxley": "https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow", | ||
| 7 | "TheJustinator": "https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw", | ||
| 8 | "chaosyumi": "https://chaosyumi.net", | ||
| 9 | "LunaPogi": "https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ", | ||
| 10 | "zeccet": "https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ", | ||
| 11 | "TheRealTinyWorld": | ||
| 12 | "https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg", | ||
| 13 | "Fred French VA": "https://www.castingcall.club/fredfrenchiii", | ||
| 14 | "rqfirqfo": "https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ", | ||
| 15 | "krispykarim": "https://www.youtube.com/@krispykarim", | ||
| 16 | "SimplyDoodled Studios": | ||
| 17 | "https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg", | ||
| 18 | "paper clover": "", | ||
| 19 | "Coffee": "https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg", | ||
| 20 | "harjjw": "https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw", | ||
| 21 | "yellowmarkers": "https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ", | ||
| 22 | "TheNothingGuy07": "https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg", | ||
| 23 | "bill wurtz": "https://billwurtz.com", | ||
| 24 | }; | ||
| 25 | |||
| 26 | const animators: [string, number][] = [ | ||
| 27 | ["atamai", 0 * 60 + 3.1], | ||
| 28 | ["TheJustinator", 1 * 60 + 2], | ||
| 29 | ["chaosyumi", 1 * 60 + 33], | ||
| 30 | ["TheJustinator", 2 * 60 + 1], | ||
| 31 | ["zeccet", 2 * 60 + 22], | ||
| 32 | ["TheRealTinyWorld", 3 * 60 + 10], | ||
| 33 | ["LunaPogi", 3 * 60 + 35], | ||
| 34 | ["moxley", 3 * 60 + 50], | ||
| 35 | ["rqfirqfo", 4 * 60 + 3], | ||
| 36 | ["krispykarim", 4 * 60 + 35], | ||
| 37 | ["SimplyDoodled Studios", 5 * 60 + 4], | ||
| 38 | ["chaosyumi", 5 * 60 + 33], | ||
| 39 | ["paper clover", 6 * 60 + 4], | ||
| 40 | ["harjjw", 6 * 60 + 37], | ||
| 41 | ["yellowmarkers", 7 * 60 + 3], | ||
| 42 | ["TheNothingGuy07", 7 * 60 + 36], | ||
| 43 | ["LunaPogi", 8 * 60 + 0], | ||
| 44 | ["paper clover", 9 * 60 + 11], | ||
| 45 | ]; | ||
| 46 | |||
| 47 | const narrators: [string, number][] = [ | ||
| 48 | ["moxley", 0 * 60 + 3.1], | ||
| 49 | ["Fred French VA", 3 * 60 + 10], | ||
| 50 | ["krispykarim", 4 * 60 + 35], | ||
| 51 | ["LunaPogi", 8 * 60 + 0], | ||
| 52 | ]; | ||
| 53 | |||
| 54 | const musicians: [string, number][] = [ | ||
| 55 | ["moxley", 0 * 60 + 3.1], | ||
| 56 | ["TheJustinator", 3 * 60 + 10], | ||
| 57 | ["LunaPogi", 6 * 60 + 4], | ||
| 58 | ["bill wurtz", 9 * 60 + 11], | ||
| 59 | ]; | ||
| 60 | |||
| 61 | // random colors for each creator | ||
| 62 | const creatorHues: Record<string, number> = {}; | ||
| 63 | for (const name of Object.keys(urls)) { | ||
| 64 | let hash = 0; | ||
| 65 | for (let i = 0; i < name.length; i++) { | ||
| 66 | hash = ((hash << 5) - hash) + name.charCodeAt(i); | ||
| 67 | hash |= 0; | ||
| 68 | } | ||
| 69 | creatorHues[name] = hash % 360; | ||
| 70 | } | ||
| 71 | |||
| 72 | function findCurrent(list: [string, number][], time: number): string | null { | ||
| 73 | let current: string | null = null; | ||
| 74 | for (const [name, start] of list) { | ||
| 75 | if (time >= start) { | ||
| 76 | current = name; | ||
| 77 | } else { | ||
| 78 | break; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | return current; | ||
| 82 | } | ||
| 83 | |||
| 84 | function createCreditElement(name: string, role: string): string { | ||
| 85 | const hue = creatorHues[name] ?? 0; | ||
| 86 | const url = urls[name]; | ||
| 87 | const style = | ||
| 88 | `background: hsl(${hue}, 60%, 25%); color: hsl(${hue}, 80%, 85%);`; | ||
| 89 | |||
| 90 | if (url) { | ||
| 91 | return `<a class="custom" href="${url}" target="_blank" style="${style}">${name} <small>(${role})</small></a>`; | ||
| 92 | } | ||
| 93 | return `<a style="${style}">${name} <small>(${role})</small></a>`; | ||
| 94 | } | ||
| 95 | |||
| 96 | const creditsBox = document.getElementById("live-credits")!; | ||
| 97 | |||
| 98 | let lastCreditsKey = ""; | ||
| 99 | |||
| 100 | function updateCredits(video) { | ||
| 101 | const time = video.currentTime; | ||
| 102 | |||
| 103 | const animator = time > 9 * 60 + 32 ? null : findCurrent(animators, time); | ||
| 104 | const narrator = time > 9 * 60 + 11 ? null : findCurrent(narrators, time); | ||
| 105 | const musician = time > 9 * 60 + 32 ? null : findCurrent(musicians, time); | ||
| 106 | |||
| 107 | // group roles by person | ||
| 108 | const rolesByPerson: Record<string, string[]> = {}; | ||
| 109 | if (animator) (rolesByPerson[animator] ??= []).push("animation"); | ||
| 110 | if (narrator) (rolesByPerson[narrator] ??= []).push("narration"); | ||
| 111 | if (musician) (rolesByPerson[musician] ??= []).push("music"); | ||
| 112 | |||
| 113 | const creditsKey = JSON.stringify(rolesByPerson); | ||
| 114 | if (creditsKey === lastCreditsKey) return; | ||
| 115 | lastCreditsKey = creditsKey; | ||
| 116 | |||
| 117 | const html = Object.entries(rolesByPerson) | ||
| 118 | .map(([name, roles]) => createCreditElement(name, roles.join(", "))) | ||
| 119 | .join(" "); | ||
| 120 | |||
| 121 | creditsBox.innerHTML = html | ||
| 122 | ? "<b style='flex:none;padding:0 8px'>credits: </b>" + html | ||
| 123 | : ""; | ||
| 124 | } | ||
| 125 | |||
| 126 | globalThis.onCloverVideoInit = (id: string, video: HTMLVideoElement) => { | ||
| 127 | console.log(1, id, video); | ||
| 128 | if (id === "main-player") { | ||
| 129 | updateCredits(video); | ||
| 130 | video.addEventListener("timeupdate", () => { | ||
| 131 | updateCredits(video); | ||
| 132 | }); | ||
| 133 | video.addEventListener("seeked", () => { | ||
| 134 | updateCredits(video); | ||
| 135 | }); | ||
| 136 | } | ||
| 137 | }; | ||
src/pages/history-of-japan-reanimated.css created+38| ... | @@ -0,0 +1,38 @@ | ||
| 1 | #live-credits { | ||
| 2 | background: #602945; | ||
| 3 | color: white; | ||
| 4 | height: 30px; | ||
| 5 | display: flex; | ||
| 6 | margin-top: -16px; | ||
| 7 | align-items: center; | ||
| 8 | } | ||
| 9 | #live-credits a { | ||
| 10 | flex: auto; | ||
| 11 | padding: 0 8px; | ||
| 12 | height: 100%; | ||
| 13 | display: flex; | ||
| 14 | align-items: center; | ||
| 15 | gap: 5px; | ||
| 16 | } | ||
| 17 | |||
| 18 | @media (max-width: 768px) { | ||
| 19 | #live-credits { | ||
| 20 | flex-direction: column; | ||
| 21 | height: unset; | ||
| 22 | |||
| 23 | b { | ||
| 24 | flex: auto; | ||
| 25 | padding: 0 8px; | ||
| 26 | display: flex; | ||
| 27 | align-items: center; | ||
| 28 | gap: 5px; | ||
| 29 | height: 30px; | ||
| 30 | } | ||
| 31 | |||
| 32 | a { | ||
| 33 | height: 30px; | ||
| 34 | width: 100%; | ||
| 35 | justify-content: center; | ||
| 36 | } | ||
| 37 | } | ||
| 38 | } | ||
src/pages/history-of-japan-reanimated.marko created+104| ... | @@ -0,0 +1,104 @@ | ||
| 1 | import "./history-of-japan-reanimated.css"; | ||
| 2 | |||
| 3 | client import "./history-of-japan-reanimated.client.ts"; | ||
| 4 | |||
| 5 | export const meta = { | ||
| 6 | title: "history of japan reanimated", | ||
| 7 | description: "17 artists and musicians come together to remake bill wurtz's legendary \"history of japan\" to celebrate its 10 year anniversary.", | ||
| 8 | embed: { | ||
| 9 | thumbnail: "https://paperclover.net/file/2026/history%20of%20japan%20reanimated/thumbnail.jpeg" | ||
| 10 | }, | ||
| 11 | canonical: "/history-of-japan-reanimated", | ||
| 12 | twitter: { | ||
| 13 | image: "https://paperclover.net/file/2026/history%20of%20japan%20reanimated/thumbnail.jpeg" | ||
| 14 | }, | ||
| 15 | }; | ||
| 16 | export const theme = { | ||
| 17 | fg: "#fff", | ||
| 18 | bg: "#442233", | ||
| 19 | primary: "#ff8b47" | ||
| 20 | }; | ||
| 21 | |||
| 22 | <main> | ||
| 23 | <h1>history of japan reanimated</h1> | ||
| 24 | <p> | ||
| 25 | me and 16 other artists (music, narration, and animation) have been | ||
| 26 | spending the past couple months on a recreation of bill wurtz's legendary | ||
| 27 | "<a href="https://billwurtz.com/history-of-japan.html">history of japan</a>" to celebrate the 10 year anniversary of his release. i | ||
| 28 | contributed two animation sections. the canonical release is on | ||
| 29 | <a href="https://youtu.be/nqsoti-OuRM">LunaPogi's YouTube page</a>, but i | ||
| 30 | am providing this ad-free video player. | ||
| 31 | </p> | ||
| 32 | |||
| 33 | <clover-video | ||
| 34 | id="main-player" | ||
| 35 | file="/2026/history of japan reanimated/history of japan reanimated.mp4" | ||
| 36 | poster="/2026/history of japan reanimated/thumbnail.jpeg" | ||
| 37 | > | ||
| 38 | <@header>history of japan reanimated</> | ||
| 39 | </> | ||
| 40 | |||
| 41 | <div id="live-credits"> | ||
| 42 | |||
| 43 | |||
| 44 | </div> | ||
| 45 | |||
| 46 | <p> | ||
| 47 | the wonderful souls who worked on the project, in order of appearance. | ||
| 48 | </p> | ||
| 49 | <ul> | ||
| 50 | <li><a href="https://www.youtube.com/channel/UClywJ-scBa8hcCywQ_7gWbg">Kappacap</a> - thumbnail artist</li> | ||
| 51 | <li><a href="https://www.tumblr.com/atamai-m8">atamai</a> - animation <span style="color:#fff9">(0:03-1:02)</></li> | ||
| 52 | <li><a href="https://www.youtube.com/channel/UCNJ6EbYQcnqpB6cQLGYXQow">moxley</a> - music and narration <span style="color:#fff9">(0:03-3:10)</>, animation <span style="color:#fff9">(3:51-4:03)</></li> | ||
| 53 | <li><a href="https://www.youtube.com/channel/UCdy0IJXbdKyTWyKajyEvodw">TheJustinator</a> - animation <span style="color:#fff9">(1:03-1:33, 2:02-2:33)</>, music <span style="color:#fff9">(3:11-6:04)</></li> | ||
| 54 | <li><a href="https://chaosyumi.net">chaosyumi</a> - animation <span style="color:#fff9">(1:34-2:01, 5:34-6:05)</></li> | ||
| 55 | <li><a href="https://www.youtube.com/channel/UC7NHQWu3awWcC0rm8nB5CfQ">LunaPogi</a> - animation <span style="color:#fff9">(3:36-3:51, 8:01-9:10)</>, music <span style="color:#fff9">(6:05-9:10)</>, narration <span style="color:#fff9">(8:01-9:10)</></li> | ||
| 56 | <li><a href="https://www.youtube.com/channel/UChec240BfC1UKX9DvBBIyLQ">zeccet (bgongo)</a> - animation <span style="color:#fff9">(2:23-3:10)</></li> | ||
| 57 | <li><a href="https://www.youtube.com/channel/UCLfv-6_J0WLsA2jAsJxiLOg">TheRealTinyWorld</a> - animation <span style="color:#fff9">(3:11-3:35)</></li> | ||
| 58 | <li><a href="https://www.castingcall.club/fredfrenchiii">Fred French VA</a> - narration <span style="color:#fff9">(3:11-4:35)</></li> | ||
| 59 | <li><a href="https://www.youtube.com/channel/UCqeAGMquQNtrSfd-quZX3KQ">rqfirqfo</a> - animation <span style="color:#fff9">(4:04-4:35)</></li> | ||
| 60 | <li><a href="https://www.youtube.com/@krispykarim">krispykarim</a> - animation <span style="color:#fff9">(4:36 - 5:04)</>, narration <span style="color:#fff9">(4:36-8:01)</></li> | ||
| 61 | <li><a href="https://www.youtube.com/channel/UCkhbgaqiIkliOKyEORnkhDg">SimplyDoodled Studios</a> - animation <span style="color:#fff9">(5:05-5:33)</></li> | ||
| 62 | <li><span style="color:#ff7de9">paper clover (me)</span> - animation <span style="color:#fff9">(6:05-6:38)</>, credits animation <span style="color:#fff9">(9:11-9:35)</></li> | ||
| 63 | <li><a href="https://www.youtube.com/channel/UCftnPCmG6dfQ50YOMRMtkjg">Coffee</a> - transcription <span style="color:#fff9">(6:05-9:10)</></li> | ||
| 64 | <li><a href="https://www.youtube.com/channel/UCPQv3ueB-E771gnwcgALzxw">harjjw</a> - animation <span style="color:#fff9">(6:38-7:03)</></li> | ||
| 65 | <li><a href="https://www.youtube.com/channel/UCcR5G761gkEKvWskjEqWFkQ">yellowmarkers</a> - animation <span style="color:#fff9">(7:04-7:36)</></li> | ||
| 66 | <li><a href="https://www.youtube.com/channel/UC8SZGQ32RQxiQeTnQ5hgYAg">TheNothingGuy07</a> - animation <span style="color:#fff9">(7:37-7:59)</></li> | ||
| 67 | </ul> | ||
| 68 | |||
| 69 | <p> | ||
| 70 | the music during the credits is an instrumental by bill wurtz: <a href="https://billwurtz.com/hey-mom-i-beat-the-scale.mp3">hey mom i beat the scale!</a> | ||
| 71 | </p> | ||
| 72 | |||
| 73 | <h2>what's next</h2> | ||
| 74 | <p> | ||
| 75 | from the success of this project, a much more ambitous project is being | ||
| 76 | planned: the reanimation for 'history of the entire world, i guess'. while | ||
| 77 | some planning between co-hosts is in progress, i am putting out <a href="https://docs.google.com/forms/d/e/1FAIpQLSdMdwotGKmrDYT8stYPm5uoOlmqZPLApYp8MuABDmcAwW6Fig/viewform">an | ||
| 78 | interest form</a> if this project is something you're interested in working | ||
| 79 | on. by submitting the form, i'll be able to reach out when it's actually time to start cooking. | ||
| 80 | </p> | ||
| 81 | |||
| 82 | <h2>behind the scenes</h2> | ||
| 83 | <p> | ||
| 84 | here is a 55 minute dive into my process, as well as nearly every little trick i used on this project. additionally, for one of the first times from me, a free download for <a href="/file/2026/history%20of%20japan%20reanimated/project%20files/history-of-japan-reanimated.zip?dl">all of the project files</a>. | ||
| 85 | </p> | ||
| 86 | <clover-video | ||
| 87 | file="/2026/history of japan reanimated/behind-the-scenes.mp4" | ||
| 88 | > | ||
| 89 | <@header>behind the scenes session with clover</> | ||
| 90 | </> | ||
| 91 | |||
| 92 | |||
| 93 | <h2>more links</h2> | ||
| 94 | <p> | ||
| 95 | <strong>more</strong>: <br /> | ||
| 96 | <a href="/file/2026/history%20of%20japan%20reanimated/fragments">fragments</a> | ||
| 97 | <br /> | ||
| 98 | <a href="/q+a">paper clover q&a page</a><br/> | ||
| 99 | <a href="/subscribe">paper clover's mailing list</a> | ||
| 100 | </p> | ||
| 101 | |||
| 102 | <h2>mentions on the q&a</h2> | ||
| 103 | <questions-embed="history-of-japan-reanimated" /> | ||
| 104 | </main> | ||
src/pages/index.marko+1| ... | @@ -21,6 +21,7 @@ export const meta = { | ... | @@ -21,6 +21,7 @@ export const meta = { |
| 21 | this site contains my art. | 21 | this site contains my art. |
| 22 | </p> | 22 | </p> |
| 23 | <h2>posts</h2> | 23 | <h2>posts</h2> |
| 24 | <p>video collab: <a href="/history-of-japan-reanimated">history of japan reanimated</a> (2026-02-02)</p> | ||
| 24 | <p style='display:flex'><span style='flex:0 0 max-content'>blog:&nbsp;</span><span style='flex: 1 0 0px'><a href="/blog/webdev/one-year-next-app-router">One Year with Next.js App Router — why we're moving on</a> (2025-10-21)</span></p> | 25 | <p style='display:flex'><span style='flex:0 0 max-content'>blog:&nbsp;</span><span style='flex: 1 0 0px'><a href="/blog/webdev/one-year-next-app-router">One Year with Next.js App Router — why we're moving on</a> (2025-10-21)</span></p> |
| 25 | <p>song: <a href="/in-the-summer">in the summer</a> (2025-08-08)</p> | 26 | <p>song: <a href="/in-the-summer">in the summer</a> (2025-08-08)</p> |
| 26 | <p>song: <a href="/waterfalls">waterfalls</a> (2025-01-01)</p> | 27 | <p>song: <a href="/waterfalls">waterfalls</a> (2025-01-01)</p> |
src/tags/clover-video.client.ts+140-38| ... | @@ -2,6 +2,9 @@ | ... | @@ -2,6 +2,9 @@ |
| 2 | 2 | ||
| 3 | // Polyfill for old browsers. | 3 | // Polyfill for old browsers. |
| 4 | type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none"; | 4 | type PlaybackMode = "av1" | "hls-polyfill" | "hls-native" | "none"; |
| 5 | const forceMode = location.search.match(/(?:\?|&)force-player=(.*)/)?.[1] as | ||
| 6 | | PlaybackMode | ||
| 7 | | undefined; | ||
| 5 | let mode: Promise<PlaybackMode> | null = null; | 8 | let mode: Promise<PlaybackMode> | null = null; |
| 6 | let hls: Promise<typeof import("hls.js")> | null = null; | 9 | let hls: Promise<typeof import("hls.js")> | null = null; |
| 7 | 10 | ||
| ... | @@ -13,6 +16,7 @@ Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) => | ... | @@ -13,6 +16,7 @@ Promise.allSettled ??= <T>(promises: Iterable<T | PromiseLike<T>>) => |
| 13 | (reason) => ({ status: "rejected" as const, reason }), | 16 | (reason) => ({ status: "rejected" as const, reason }), |
| 14 | ))); | 17 | ))); |
| 15 | 18 | ||
| 19 | declare function hydrateVideo(container: HTMLElement): void; | ||
| 16 | globalThis.hydrateVideo = function hydrateVideo(container) { | 20 | globalThis.hydrateVideo = function hydrateVideo(container) { |
| 17 | mode ??= decidePlaybackMode(); | 21 | mode ??= decidePlaybackMode(); |
| 18 | mode.then((mode) => hydrateVideoInner(container, mode)); | 22 | mode.then((mode) => hydrateVideoInner(container, mode)); |
| ... | @@ -23,9 +27,21 @@ Array.from( | ... | @@ -23,9 +27,21 @@ Array.from( |
| 23 | (container) => hydrateVideo(container), | 27 | (container) => hydrateVideo(container), |
| 24 | ); | 28 | ); |
| 25 | 29 | ||
| 30 | // TODO: be less stupid about this | ||
| 31 | const onCloverVideoInit = window.onCloverVideoInit; | ||
| 32 | |||
| 26 | function decidePlaybackMode(): Promise<PlaybackMode> { | 33 | function decidePlaybackMode(): Promise<PlaybackMode> { |
| 34 | if (forceMode) { | ||
| 35 | if (forceMode === "hls-polyfill") { | ||
| 36 | // @ts-expect-error | ||
| 37 | return (hls ??= import("/js/scripts/vendor/hls.js")) | ||
| 38 | .then(() => forceMode); | ||
| 39 | } | ||
| 40 | return Promise.resolve(forceMode); | ||
| 41 | } | ||
| 42 | |||
| 27 | return shouldPlayAv1().catch(() => ({ hardware: false, supported: false })) | 43 | return shouldPlayAv1().catch(() => ({ hardware: false, supported: false })) |
| 28 | .then((av1): Awaitable<PlaybackMode> => { | 44 | .then((av1): PlaybackMode | Promise<PlaybackMode> => { |
| 29 | // Native AV1 with DASH Polyfill | 45 | // Native AV1 with DASH Polyfill |
| 30 | // (Chrome v70, Safari v17 on M3/A17 chip, Firefox v67) | 46 | // (Chrome v70, Safari v17 on M3/A17 chip, Firefox v67) |
| 31 | if (av1.hardware) return "av1"; | 47 | if (av1.hardware) return "av1"; |
| ... | @@ -43,6 +59,7 @@ function decidePlaybackMode(): Promise<PlaybackMode> { | ... | @@ -43,6 +59,7 @@ function decidePlaybackMode(): Promise<PlaybackMode> { |
| 43 | return "hls-native"; | 59 | return "hls-native"; |
| 44 | } | 60 | } |
| 45 | 61 | ||
| 62 | // @ts-expect-error | ||
| 46 | return (hls ??= import("/js/scripts/vendor/hls.js")) | 63 | return (hls ??= import("/js/scripts/vendor/hls.js")) |
| 47 | .then((hls): PlaybackMode => { | 64 | .then((hls): PlaybackMode => { |
| 48 | // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8) | 65 | // Polyfill HLS (Chrome 23, Firefox 42, IE11 on Win8) |
| ... | @@ -59,80 +76,165 @@ function decidePlaybackMode(): Promise<PlaybackMode> { | ... | @@ -59,80 +76,165 @@ function decidePlaybackMode(): Promise<PlaybackMode> { |
| 59 | }); | 76 | }); |
| 60 | } | 77 | } |
| 61 | 78 | ||
| 62 | declare global { | ||
| 63 | function hydrateVideo(container: HTMLElement): void; | ||
| 64 | } | ||
| 65 | |||
| 66 | function hydrateVideoInner( | 79 | function hydrateVideoInner( |
| 67 | container: HTMLElement, | 80 | container: HTMLElement, |
| 68 | mode: PlaybackMode, | 81 | mode: PlaybackMode, |
| 69 | ) { | 82 | ) { |
| 70 | const video = container instanceof HTMLVideoElement | 83 | let video = container instanceof HTMLVideoElement |
| 71 | ? container | 84 | ? container |
| 72 | : container.querySelector("video")!; | 85 | : container.querySelector("video")!; |
| 86 | const id = video.id; | ||
| 73 | if (video.hasAttribute("controls")) return; | 87 | if (video.hasAttribute("controls")) return; |
| 74 | let src = video.getAttribute("data-src"); | 88 | let src = video.getAttribute("data-src"); |
| 75 | if (!src) return; | 89 | if (!src) return; |
| 76 | src = `/file${src}`; | 90 | src = `/file${src}`; |
| 91 | src = new URL(src, location.origin).href; | ||
| 77 | 92 | ||
| 78 | const dashFile = `${src}:/dash.mpd`; | 93 | const dashFile = `${src}:/dash.mpd`; |
| 79 | const hlsFile = `${src}:/master.m3u8`; | 94 | const hlsFile = `${src}:/master.m3u8`; |
| 80 | 95 | ||
| 96 | if (forceMode) { | ||
| 97 | const figcaption = container.querySelector("figcaption"); | ||
| 98 | if (figcaption) { | ||
| 99 | figcaption.innerHTML += "&nbsp;(force player backend: " + forceMode + ")"; | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 81 | if (mode === "av1") { | 103 | if (mode === "av1") { |
| 104 | // @ts-expect-error | ||
| 82 | import("/js/scripts/vendor/dash.js").then((dashjs) => { | 105 | import("/js/scripts/vendor/dash.js").then((dashjs) => { |
| 83 | const player = dashjs.MediaPlayer().create(); | ||
| 84 | // This proxying is for ManagedMediaSource, exposing the HLS stream for AirPlay | ||
| 85 | let mainSource: HTMLSourceElement | null = null; | 106 | let mainSource: HTMLSourceElement | null = null; |
| 86 | Object.defineProperty(video, "src", { | 107 | let mediaSource: MediaSource | null = null, objectUrl = ""; |
| 87 | get: () => false, | 108 | const hasThumbnail = video.hasAttribute("poster"); |
| 88 | set: (v) => { | 109 | |
| 89 | if (!mainSource) { | 110 | const player = dashjs.MediaPlayer().create(); |
| 90 | mainSource = document.createElement("source"); | 111 | |
| 91 | mainSource.src = v; | 112 | // the world is so simple when there is no thumbnail because we are not |
| 92 | video.appendChild(mainSource); | 113 | // working around a billion sharp edges. |
| 93 | } else { | 114 | if (!hasThumbnail) { |
| 94 | mainSource.src = v; | 115 | enableSafariAirplay(); |
| 95 | } | 116 | player.initialize(video, dashFile, false); |
| 96 | }, | 117 | video.controls = true; |
| 118 | onCloverVideoInit?.(id, video); | ||
| 119 | return; | ||
| 120 | } | ||
| 121 | |||
| 122 | // With Dash.js, we get pranked. No matter if preloading is on or off, | ||
| 123 | // the first frame is always preloaded and will override the poster. | ||
| 124 | // To avoid this, the viewer is attached later. However, on Chrome the | ||
| 125 | // video is still not playable until metadata has loaded in, so a fake | ||
| 126 | // MediaSource with one invisible frame is used. | ||
| 127 | player.initialize(null, dashFile, false); | ||
| 128 | player.updateSettings({ | ||
| 129 | streaming: { cacheInitSegments: true }, | ||
| 97 | }); | 130 | }); |
| 98 | Object.defineProperty(video, "disableRemotePlayback", { | 131 | player.preload(); |
| 99 | get: () => false, | 132 | |
| 100 | set: () => { | 133 | if (typeof MediaSource !== "undefined") { |
| 101 | const source = document.createElement("source"); | 134 | const duration = new Promise<number>((resolve) => { |
| 102 | source.src = hlsFile; | 135 | player.on(dashjs.MediaPlayer.events.MANIFEST_LOADED, (e) => { |
| 103 | video.appendChild(source); | 136 | const duration = e.data.mediaPresentationDuration; |
| 104 | }, | 137 | resolve(duration); // seconds |
| 138 | }); | ||
| 139 | }); | ||
| 140 | mediaSource = new MediaSource(); | ||
| 141 | mediaSource.addEventListener("sourceopen", () => { | ||
| 142 | duration.then((duration) => { | ||
| 143 | mediaSource = mediaSource!; | ||
| 144 | mediaSource.duration = duration; | ||
| 145 | const sb = mediaSource.addSourceBuffer('video/webm; codecs="vp8"'); | ||
| 146 | const oneFrame = Uint8Array.from( | ||
| 147 | atob( | ||
| 148 | "GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAITEU2bdLpNu4tTq4QVSalmU6yBoU27i1OrhBZUrmtTrIHWTbuMU6uEElTDZ1OsggEjTbuMU6uEHFO7a1OsggH97AEAAAAAAABZ" + | ||
| 149 | "A".repeat(119) + | ||
| 150 | "VSalmsCrXsYMPQkBNgIxMYXZmNjIuMy4xMDBXQYxMYXZmNjIuMy4xMDBEiYhAXgAAAAAAABZUrmvIrgEAAAAAAAA/14EBc8WIDV7YG1KxA/CcgQAitZyDdW5kiIEAhoVWX1ZQOIOBASPjg4QCYloA4JCwgQG6gQGagQJVsIRVuYEBElTDZ/tzc59jwIBnyJlFo4dFTkNPREVSRIeMTGF2ZjYyLjMuMTAwc3PWY8CLY8WIDV7YG1KxA/BnyKFFo4dFTkNPREVSRIeUTGF2YzYyLjExLjEwMCBsaWJ2cHhnyKFFo4hEVVJBVElPTkSHkzAwOjAwOjAwLjEyMDAwMDAwMAAfQ7Z11eeBAKOigQAAgBACAJ0BKgEAAQALxwiFhYiFhIg/ggAMDWAA/ua1AKOVgQAoALEBAC8R/AAYABhYL/QAJAAAo5WBAFAAsQEALxH8ABgAGFgv9AAkAAAcU7trkbuPs4EAt4r3gQHxggGj8IED", | ||
| 151 | ), | ||
| 152 | (x) => x.charCodeAt(0), | ||
| 153 | ); | ||
| 154 | sb.appendBuffer(oneFrame); | ||
| 155 | }); | ||
| 156 | }); | ||
| 157 | objectUrl = URL.createObjectURL(mediaSource); | ||
| 158 | |||
| 159 | mainSource = document.createElement("source"); | ||
| 160 | mainSource.src = objectUrl; | ||
| 161 | video.appendChild(mainSource); | ||
| 162 | } | ||
| 163 | video.addEventListener("play", function play() { | ||
| 164 | video.removeEventListener("play", play); | ||
| 165 | |||
| 166 | // Revoke Fake MediaSource | ||
| 167 | if (objectUrl) { | ||
| 168 | mainSource?.remove(); | ||
| 169 | mainSource = null; | ||
| 170 | mediaSource?.endOfStream(); | ||
| 171 | mediaSource = null; | ||
| 172 | URL.revokeObjectURL(objectUrl); | ||
| 173 | |||
| 174 | const newVideo = document.createElement("video"); | ||
| 175 | newVideo.poster = video.poster; | ||
| 176 | newVideo.controls = true; | ||
| 177 | video.parentElement!.insertBefore(newVideo, video); | ||
| 178 | video.remove(); | ||
| 179 | video = newVideo; | ||
| 180 | } | ||
| 181 | |||
| 182 | enableSafariAirplay(); | ||
| 183 | player.attachView(video); | ||
| 184 | if (objectUrl) video.play(); | ||
| 185 | onCloverVideoInit?.(id, video); | ||
| 105 | }); | 186 | }); |
| 106 | player.initialize( | ||
| 107 | video, | ||
| 108 | dashFile, | ||
| 109 | false, // autoplay | ||
| 110 | 0, // start | ||
| 111 | ); | ||
| 112 | video.controls = true; | 187 | video.controls = true; |
| 188 | onCloverVideoInit?.(id, video); | ||
| 189 | |||
| 190 | // This proxying is for ManagedMediaSource, exposing the HLS stream for AirPlay | ||
| 191 | function enableSafariAirplay() { | ||
| 192 | Object.defineProperty(video, "src", { | ||
| 193 | get: () => false, | ||
| 194 | set: (v) => { | ||
| 195 | if (!mainSource) { | ||
| 196 | mainSource = document.createElement("source"); | ||
| 197 | mainSource.src = v; | ||
| 198 | video.appendChild(mainSource); | ||
| 199 | } else { | ||
| 200 | mainSource.src = v; | ||
| 201 | } | ||
| 202 | }, | ||
| 203 | }); | ||
| 204 | Object.defineProperty(video, "disableRemotePlayback", { | ||
| 205 | get: () => false, | ||
| 206 | set: () => { | ||
| 207 | const source = document.createElement("source"); | ||
| 208 | source.src = hlsFile; | ||
| 209 | video.appendChild(source); | ||
| 210 | }, | ||
| 211 | }); | ||
| 212 | } | ||
| 113 | }); | 213 | }); |
| 114 | } else if (mode === "hls-native") { | 214 | } else if (mode === "hls-native") { |
| 115 | const hlsSource = document.createElement("source"); | 215 | const hlsSource = document.createElement("source"); |
| 116 | hlsSource.src = `${src}:/hls.m3u8`; | 216 | hlsSource.src = hlsFile; |
| 117 | video.appendChild(hlsSource); | 217 | video.appendChild(hlsSource); |
| 118 | video.controls = true; | 218 | video.controls = true; |
| 219 | onCloverVideoInit?.(id, video); | ||
| 119 | } else if (mode === "hls-polyfill") { | 220 | } else if (mode === "hls-polyfill") { |
| 120 | hls!.then((hls) => { | 221 | hls!.then((hls) => { |
| 121 | const player = new hls.default(); | 222 | console.log(hls); |
| 223 | // @ts-expect-error suspicious | ||
| 224 | const player = new (hls.default ?? hls.Hls)(); | ||
| 122 | player.loadSource(hlsFile); | 225 | player.loadSource(hlsFile); |
| 123 | player.attachMedia(video); | 226 | player.attachMedia(video); |
| 124 | video.controls = true; | 227 | video.controls = true; |
| 228 | onCloverVideoInit?.(id, video); | ||
| 125 | }); | 229 | }); |
| 126 | } else { | 230 | } else { |
| 127 | video.src = src; | 231 | video.src = src; |
| 128 | video.controls = true; | 232 | video.controls = true; |
| 233 | onCloverVideoInit?.(id, video); | ||
| 129 | } | 234 | } |
| 130 | } | 235 | } |
| 131 | 236 | ||
| 132 | declare global { | 237 | declare var ManagedMediaSource: typeof MediaSource | null; |
| 133 | var ManagedMediaSource: typeof MediaSource | null; | ||
| 134 | } | ||
| 135 | |||
| 136 | function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> { | 238 | function shouldPlayAv1(): Promise<{ hardware: boolean; supported: boolean }> { |
| 137 | const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource; | 239 | const MediaSource = globalThis.ManagedMediaSource ?? globalThis.MediaSource; |
| 138 | if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) { | 240 | if (!MediaSource.isTypeSupported('audio/webm;codecs="opus"')) { |
src/tags/clover-video.css+1| ... | @@ -18,6 +18,7 @@ | ... | @@ -18,6 +18,7 @@ |
| 18 | } | 18 | } |
| 19 | .clover-video video { | 19 | .clover-video video { |
| 20 | width: 100%; | 20 | width: 100%; |
| 21 | height: 100%; | ||
| 21 | } | 22 | } |
| 22 | .clover-video h2 { | 23 | .clover-video h2 { |
| 23 | margin-top: 0; | 24 | margin-top: 0; |
src/tags/clover-video.marko+3-1| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import "./clover-video.css"; | 1 | import "./clover-video.css"; |
| 2 | 2 | ||
| 3 | export interface Input { | 3 | export interface Input { |
| 4 | id?: string; | ||
| 4 | file: MediaFile | string; | 5 | file: MediaFile | string; |
| 5 | poster?: MediaFile | string; | 6 | poster?: MediaFile | string; |
| 6 | header?: Marko.AttrTag<{ content: Marko.Body }>; | 7 | header?: Marko.AttrTag<{ content: Marko.Body }>; |
| ... | @@ -8,7 +9,7 @@ export interface Input { | ... | @@ -8,7 +9,7 @@ export interface Input { |
| 8 | noFigure?: boolean | 9 | noFigure?: boolean |
| 9 | } | 10 | } |
| 10 | 11 | ||
| 11 | <const/{ header, minimal, noFigure } = input /> | 12 | <const/{ id, header, minimal, noFigure } = input /> |
| 12 | <const/file = typeof UNWRAP(input.file) === 'object' | 13 | <const/file = typeof UNWRAP(input.file) === 'object' |
| 13 | ? input.file | 14 | ? input.file |
| 14 | : UNWRAP(MediaFile.getByPath(input.file), `No file ${JSON.stringify(input.file)}`) /> | 15 | : UNWRAP(MediaFile.getByPath(input.file), `No file ${JSON.stringify(input.file)}`) /> |
| ... | @@ -22,6 +23,7 @@ export interface Input { | ... | @@ -22,6 +23,7 @@ export interface Input { |
| 22 | 23 | ||
| 23 | <define/Inner> | 24 | <define/Inner> |
| 24 | <video | 25 | <video |
| 26 | ...{ id } | ||
| 25 | class={ 'clover-video': noFigure } | 27 | class={ 'clover-video': noFigure } |
| 26 | data-src=file.path | 28 | data-src=file.path |
| 27 | poster=poster ? `/file${poster.path}` : null | 29 | poster=poster ? `/file${poster.path}` : null |
tsconfig.json+1-1| ... | @@ -18,5 +18,5 @@ | ... | @@ -18,5 +18,5 @@ |
| 18 | "noUncheckedIndexedAccess": true, | 18 | "noUncheckedIndexedAccess": true, |
| 19 | "resolveJsonModule": true | 19 | "resolveJsonModule": true |
| 20 | }, | 20 | }, |
| 21 | "include": ["lib/**/*"] | 21 | "include": ["lib/**/*", "src/**/*", "framework/**/*"] |
| 22 | } | 22 | } |