diff --git a/framework/generate.ts b/framework/generate.ts index f4e6873c7af97f11c80baef073e90b65c277e464..04fda5eadca1eac4b8dab9dc6525d06730e1b209 100644 --- a/framework/generate.ts +++ b/framework/generate.ts @@ -184,7 +184,7 @@ export async function scanSiteSection(io: Io, section: sg.Section) { list: pages, prefix: section.base, include: [".tsx", ".marko", ".mdo"], - exclude: [".client.ts", ".client.tsx"], + exclude: [".client.ts"], page: true, }, { @@ -198,7 +198,7 @@ export async function scanSiteSection(io: Io, section: sg.Section) { dir: sectionPath("scripts"), list: scripts, prefix: rootPrefix, - include: [".client.ts", ".client.tsx"], + include: [".client.ts"], page: false, }, { @@ -206,7 +206,7 @@ export async function scanSiteSection(io: Io, section: sg.Section) { list: views, prefix: rootPrefix, include: [".tsx", ".marko", ".mdo"], - exclude: [".client.ts", ".client.tsx"], + exclude: [".client.ts"], page: false, }, ]; diff --git a/framework/hot.ts b/framework/hot.ts index e9289532531933515ca2e38c4a4c47a8e3dd7ce9..531eeda3317b59faa6175b06ffac3cc3ae47e210 100644 --- a/framework/hot.ts +++ b/framework/hot.ts @@ -144,8 +144,8 @@ export function loadEsbuildCode( export function resolveClientRef(sourcePath: string, ref: string) { const filePath = resolveFrom(sourcePath, ref); - if (!filePath.endsWith(".client.ts") && !filePath.endsWith(".client.tsx")) { - throw new Error("addScript must be a .client.ts or .client.tsx"); + if (!filePath.endsWith(".client.ts")) { + throw new Error("addScript must take a .client.ts"); } return path.relative(projectSrc, filePath); } diff --git a/package-lock.json b/package-lock.json index f6dcf2dd0d148c14dfdc6f75dc10d2685af3f9a3..205b45cf4a32d12e66c3a1417a9eac99b0248ffb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "@marko/compiler": "5.39.62", "@types/node": "^24.12.0", "dashjs": "5.0.3", - "devalue": "5.6.4", "esbuild": "0.25.5", "harfbuzzjs": "0.10.1", "hls.js": "1.6.5", @@ -22,7 +21,7 @@ "source-map-support": "0.5.21", "typescript": "^5.9.3", "unique-names-generator": "4.7.1", - "vitest": "^4.0.18", + "vitest": "4.1.2", "vscode-oniguruma": "2.0.1", "vscode-textmate": "9.2.0", "wawoff2": "2.0.1" @@ -670,18 +669,18 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", "cpu": [ "arm64" ], @@ -695,9 +694,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", "cpu": [ "arm64" ], @@ -711,9 +710,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", "cpu": [ "x64" ], @@ -727,9 +726,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", "cpu": [ "x64" ], @@ -743,9 +742,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", "cpu": [ "arm" ], @@ -759,9 +758,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", "cpu": [ "arm64" ], @@ -775,9 +774,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", "cpu": [ "arm64" ], @@ -791,9 +790,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", "cpu": [ "ppc64" ], @@ -807,9 +806,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", "cpu": [ "s390x" ], @@ -823,9 +822,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", "cpu": [ "x64" ], @@ -839,9 +838,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", "cpu": [ "x64" ], @@ -855,9 +854,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -871,9 +870,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", "cpu": [ "wasm32" ], @@ -887,9 +886,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -903,9 +902,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -919,9 +918,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", "license": "MIT" }, "node_modules/@standard-schema/spec": { @@ -987,17 +986,17 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1013,24 +1012,24 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -1038,13 +1037,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1053,23 +1052,23 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1257,12 +1256,6 @@ "node": ">=8" } }, - "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", - "license": "MIT" - }, "node_modules/error-stack-parser": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", @@ -1952,9 +1945,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "peer": true, "engines": { @@ -2056,13 +2049,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" @@ -2071,21 +2064,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, "node_modules/sax": { @@ -2266,18 +2259,18 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -2288,8 +2281,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -2305,13 +2298,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -2747,12 +2740,12 @@ } }, "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2761,7 +2754,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -2787,16 +2780,16 @@ } }, "node_modules/vitest/node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "license": "MIT", "peer": true, "dependencies": { "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", + "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "bin": { diff --git a/package.json b/package.json index e2ecb8def63d579643a8bd6ab2ad272f7ba6fef3..7d37dc1883e43562d386e1292f585bf4dd358993 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "@marko/compiler": "5.39.62", "@types/node": "^24.12.0", "dashjs": "5.0.3", - "devalue": "5.6.4", "esbuild": "0.25.5", "harfbuzzjs": "0.10.1", "hls.js": "1.6.5", @@ -18,7 +17,7 @@ "source-map-support": "0.5.21", "typescript": "^5.9.3", "unique-names-generator": "4.7.1", - "vitest": "^4.0.18", + "vitest": "4.1.2", "vscode-oniguruma": "2.0.1", "vscode-textmate": "9.2.0", "wawoff2": "2.0.1" diff --git a/src/blog/drafts/webdev/marko-intro.markodown b/src/blog/drafts/webdev/marko-intro.markodown index 50cc917b10b37b4990cf63b60a9d78016aa270f2..9afe753c9cec12f30f9b114f39fba6eaea97ab7b 100644 --- a/src/blog/drafts/webdev/marko-intro.markodown +++ b/src/blog/drafts/webdev/marko-intro.markodown @@ -330,7 +330,7 @@ client import "./clickable-links.client.ts"; import type { Question } from "#src/q+a/models/Question.ts"; import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "#src/q+a/format.ts"; -import { QuestionMarkdown } from "#src/q+a/markdown.tsx"; +import { QuestionMarkdown } from "#src/q+a/markdown.ts"; ``` The integration is great, `client import` is quite a magical concept, and I've diff --git a/src/blog/localization.ts b/src/blog/localization.ts new file mode 100644 index 0000000000000000000000000000000000000000..16859e225863daad941858fa1af7899c0925037b --- /dev/null +++ b/src/blog/localization.ts @@ -0,0 +1,34 @@ +interface LanguageConfig { + title: render.Node; + return: render.Node; + writtenByClover: string; + translatedBy: (_: { author: string }) => render.Node; + tableOfContents: string; +} +type LanguageMap = Record & { en: LanguageConfig }; + +export const languages: LanguageMap = { + en: { + title: "English", + return: "back to the home page", + writtenByClover: `clover caruso`, // "written by clover caruso" in other languages + translatedBy: ({ author }) => `translated by ${author}`, + tableOfContents: "Contents", + }, + ko: { + title: "한국어", + return: "홈 페이지로 돌아가기", + writtenByClover: `clover caruso 작성`, + translatedBy: ({ author }) => `${author} 번역`, + tableOfContents: "Contents", + }, + es: { + title: "Español", + return: "volver a la página principal", + writtenByClover: "escrito por clover caruso", + translatedBy: ({ author }) => `traducido por ${author}`, + tableOfContents: "Indice", + }, +}; + +import * as render from "@clo/lib/render"; diff --git a/src/blog/localization.tsx b/src/blog/localization.tsx deleted file mode 100644 index 16859e225863daad941858fa1af7899c0925037b..0000000000000000000000000000000000000000 --- a/src/blog/localization.tsx +++ /dev/null @@ -1,34 +0,0 @@ -interface LanguageConfig { - title: render.Node; - return: render.Node; - writtenByClover: string; - translatedBy: (_: { author: string }) => render.Node; - tableOfContents: string; -} -type LanguageMap = Record & { en: LanguageConfig }; - -export const languages: LanguageMap = { - en: { - title: "English", - return: "back to the home page", - writtenByClover: `clover caruso`, // "written by clover caruso" in other languages - translatedBy: ({ author }) => `translated by ${author}`, - tableOfContents: "Contents", - }, - ko: { - title: "한국어", - return: "홈 페이지로 돌아가기", - writtenByClover: `clover caruso 작성`, - translatedBy: ({ author }) => `${author} 번역`, - tableOfContents: "Contents", - }, - es: { - title: "Español", - return: "volver a la página principal", - writtenByClover: "escrito por clover caruso", - translatedBy: ({ author }) => `traducido por ${author}`, - tableOfContents: "Indice", - }, -}; - -import * as render from "@clo/lib/render"; diff --git a/src/blog/tags/blog-layout.marko b/src/blog/tags/blog-layout.marko index bf042ff3f071de1a2106ec5daa1976d82dd0f694..7ca09bdf9701b898e211691adfacebb124a279f4 100644 --- a/src/blog/tags/blog-layout.marko +++ b/src/blog/tags/blog-layout.marko @@ -99,7 +99,7 @@ export const markoOnly = true; -import * as localization from "../localization.tsx"; +import * as localization from "../localization.ts"; import * as markodown from "@clo/markodown"; import Heading from "./heading.marko"; import CodeBlock from "./code-block.marko"; diff --git a/src/file-viewer/cert.pem b/src/file-viewer/cert.pem deleted file mode 100644 index 824dd0782847bcfe1c6fc15f242e8523fc9a0fac..0000000000000000000000000000000000000000 --- a/src/file-viewer/cert.pem +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIUBaaOXVkkE+6yarNyvzofETb+WLEwDQYJKoZIhvcNAQEL -BQAwdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 -MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0aDEhMB8GCSqG -SIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MB4XDTI1MDQyNzIxNTU0MFoXDTM1 -MDQyNTIxNTU0MFowdzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYD -VQQHDARDaXR5MRUwEwYDVQQKDAxNZWRpYSBTZXJ2ZXIxDzANBgNVBAMMBnplbml0 -aDEhMB8GCSqGSIb3DQEJARYSbWVAcGFwZXJjbG92ZXIubmV0MIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7lLwx8XwsuTeaIxTsHDL+Lx7eblsJ0XylVm -0/iIJS1Mrq6Be9St6vDWK/BWqqAn+MdqzSfLMy8EKazuHKtbTm2vlUIkjw28SoWP -6cRSCLx4hFGbF4tmRO+Bo+/4PpHPnheeolkjJ+CLO87tZ752D9JzjVND+WIj1QO+ -bm+JBIi1TFREPh22/fSZBRpaRgqHcUEhICaiXaufvxQ6eihQfGSe00I7zRzGgnMl -51xjzkKkXd+r/FwTykd8ScJN25FMVDLsfJR59//geAZXYS25gQ4YL6R8u7ijidlS -IoDG8N+Fzw7W4yI+y8fIN4W1x/HsjiQ665CuWY3TMYo98OaGwwIDAQABo0kwRzAm -BgNVHREEHzAdggZ6ZW5pdGiCE25hcy5wYXBlcmNsb3Zlci5uZXQwHQYDVR0OBBYE -FDXkgNsMYZv1Pr+95RCCk7eHACGOMA0GCSqGSIb3DQEBCwUAA4IBAQB6942odKyD -TudifxRXbvcVe9LxSd7NimxRZzM5wTgA5KkxQT4CBM2wEPH/7e7Q/8scB9HbH2uP -f2vixoCM+Z3BWiYHFFk+1pf2myUdiFV2BC9g80txEerRkGLc18V6CdYNJ9wNPkiO -LW/RzXfEv+sqhaXh8dA46Ruz6SAbmscTMMYW4e9VYR+1p4Sm5UpTxrHzeg21YJKn -ud8kO1r7RhVgUGzkAzNaIMiBuJqGGdD5yV7Ng5C/DlJ9AAeYu1diM5LkIKjf+/8M -t/3l4eXS3Lda6+21rDvmfoK4Za6CAhcwgXIpqiRixE2MQNsxZ2XiJBVQHPrh8xYk -L5fq8KTGFwtd ------END CERTIFICATE----- diff --git a/src/q+a/backend.ts b/src/q+a/backend.ts index 4df4c6fba697b9c3779f0b4eec8a48d63c14ddb4..a22e9a100a56226c71734f58ac0e0f6df007a8e1 100644 --- a/src/q+a/backend.ts +++ b/src/q+a/backend.ts @@ -227,5 +227,5 @@ import { serveAsset } from "#sitegen/assets"; import type { PendingQuestionData } from "./models/PendingQuestion.ts"; import { Question, QuestionType } from "./models/Question.ts"; import * as view from "#sitegen/view"; -import { getQuestionImage } from "./image.tsx"; +import { getQuestionImage } from "./image.ts"; import { formatQuestionId, questionIdToTimestamp } from "./format.ts"; diff --git a/src/q+a/image.ts b/src/q+a/image.ts new file mode 100644 index 0000000000000000000000000000000000000000..7788bc0a26b28bed5e84f4f7eb24b4e2efa70ecd --- /dev/null +++ b/src/q+a/image.ts @@ -0,0 +1,79 @@ +const port = Number(process.env.PORT ?? 3000); +const width = 768; +const cacheImageDir = path.resolve(".clover/question_images"); + +const getBrowser = RefCountedExpirable( + () => + playwright.chromium.launch({ + args: ["--no-sandbox", "--disable-setuid-sandbox"], + }), + (b) => b.close(), +); + +export async function renderQuestionImage(question: Question) { + using sharedBrowser = await getBrowser(); + const b = sharedBrowser.value; + const ctx = await b.newContext({ + userAgent: "clover-qa-image", + deviceScaleFactor: 1.5, + }); + try { + const p = await ctx.newPage(); + await p.setViewportSize({ width, height: 400 }); + await p.goto(`http://localhost:${port}/q+a/${question.id}`); + try { + await p.waitForLoadState("networkidle", { timeout: 500 }); + } catch {} + const height = await p.evaluate(() => { + document.body.style.background = "#1a1b1c"; + return document.querySelector("main")!.getBoundingClientRect().height; + }); + return await p.screenshot({ + type: "png", + clip: { x: 0, y: 0, width, height }, + }); + } finally { + await ctx.close(); + } +} + +export async function getQuestionImage( + question: Question, + headOnly: boolean, +): Promise { + const hash = crypto + .createHash("sha1") + .update(question.qmid + question.type + question.text) + .digest("hex"); + + const headers = { + "Content-Type": "image/png", + "Cache-Control": "public, max-age=31536000", + ETag: `"${hash}"`, + "Last-Modified": question.date.toUTCString(), + }; + + if (headOnly) { + return new Response(null, { headers }); + } + + const cachedFilePath = path.join(cacheImageDir, `/${hash}.png`); + let buf: Buffer; + try { + buf = await fs.readFile(cachedFilePath); + } catch (e: any) { + if (e.code !== "ENOENT") throw e; + buf = await renderQuestionImage(question); + fs.writeMkdir(cachedFilePath, buf).catch(() => {}); + } + + return new Response(buf, { headers }); +} + +import * as crypto from "node:crypto"; +import * as fs from "#sitegen/fs"; +import * as path from "node:path"; +import { Question } from "#src/q+a/models/Question.ts"; +import { RefCountedExpirable } from "@clo/lib/async"; +import * as view from "#sitegen/view"; +import * as playwright from "playwright"; diff --git a/src/q+a/image.tsx b/src/q+a/image.tsx deleted file mode 100644 index 7788bc0a26b28bed5e84f4f7eb24b4e2efa70ecd..0000000000000000000000000000000000000000 --- a/src/q+a/image.tsx +++ /dev/null @@ -1,79 +0,0 @@ -const port = Number(process.env.PORT ?? 3000); -const width = 768; -const cacheImageDir = path.resolve(".clover/question_images"); - -const getBrowser = RefCountedExpirable( - () => - playwright.chromium.launch({ - args: ["--no-sandbox", "--disable-setuid-sandbox"], - }), - (b) => b.close(), -); - -export async function renderQuestionImage(question: Question) { - using sharedBrowser = await getBrowser(); - const b = sharedBrowser.value; - const ctx = await b.newContext({ - userAgent: "clover-qa-image", - deviceScaleFactor: 1.5, - }); - try { - const p = await ctx.newPage(); - await p.setViewportSize({ width, height: 400 }); - await p.goto(`http://localhost:${port}/q+a/${question.id}`); - try { - await p.waitForLoadState("networkidle", { timeout: 500 }); - } catch {} - const height = await p.evaluate(() => { - document.body.style.background = "#1a1b1c"; - return document.querySelector("main")!.getBoundingClientRect().height; - }); - return await p.screenshot({ - type: "png", - clip: { x: 0, y: 0, width, height }, - }); - } finally { - await ctx.close(); - } -} - -export async function getQuestionImage( - question: Question, - headOnly: boolean, -): Promise { - const hash = crypto - .createHash("sha1") - .update(question.qmid + question.type + question.text) - .digest("hex"); - - const headers = { - "Content-Type": "image/png", - "Cache-Control": "public, max-age=31536000", - ETag: `"${hash}"`, - "Last-Modified": question.date.toUTCString(), - }; - - if (headOnly) { - return new Response(null, { headers }); - } - - const cachedFilePath = path.join(cacheImageDir, `/${hash}.png`); - let buf: Buffer; - try { - buf = await fs.readFile(cachedFilePath); - } catch (e: any) { - if (e.code !== "ENOENT") throw e; - buf = await renderQuestionImage(question); - fs.writeMkdir(cachedFilePath, buf).catch(() => {}); - } - - return new Response(buf, { headers }); -} - -import * as crypto from "node:crypto"; -import * as fs from "#sitegen/fs"; -import * as path from "node:path"; -import { Question } from "#src/q+a/models/Question.ts"; -import { RefCountedExpirable } from "@clo/lib/async"; -import * as view from "#sitegen/view"; -import * as playwright from "playwright"; diff --git a/src/q+a/markdown.ts b/src/q+a/markdown.ts new file mode 100644 index 0000000000000000000000000000000000000000..59ae2824b88f38f0d6d9d10bd22f32c275fddea8 --- /dev/null +++ b/src/q+a/markdown.ts @@ -0,0 +1,81 @@ +function labelledRedaction(contents: string) { + return ( + `` + + (contents === "name" + ? "old name" + : contents === "username" + ? "old username" + : contents === "domain" + ? "old domain" + : null) + + `` + ); +} + +function questionRef(id: string) { + if (id.length === 12) id = id.slice(0, 10); + const timestamp = questionIdToTimestamp(id); + if (!timestamp) { + console.error(`Invalid question ref: ${id}`); + return `[Invalid Question Ref: #${ + render.escapeHtmlContent(id) + }]`; + } + return `${ + formatQuestionTimestamp(timestamp) + }`; +} + +function artifactRef(id: string) { + const artifact = artifactMap[id]; + if (!artifact) { + return `[Invalid Artifact Ref: @${ + render.escapeHtmlContent(id) + }]`; + } + const [title, url, type] = artifact; + if (!url) { + return `${render.escapeHtmlContent(title)}`; + } + return `` + + render.escapeHtmlContent(title) + + ``; +} + +/** classic esbuild trick: use an impossible to guess key for string magic */ +const uniqueKey = crypto.randomUUID().replaceAll("-", ""); + +const renderers = { + A: artifactRef, + Q: questionRef, + R: labelledRedaction, +} as const; + +export function renderMarkdown(source: string) { + const tagNames: markodown.CloverQuestionExtensions = { + question: "q-", + artifactRef: uniqueKey + "_A", + questionRef: uniqueKey + "_Q", + labelledRedaction: uniqueKey + "_R", + }; + const result = markodown.transform({ + source, + markdownOnly: true, + cloverExtensions: tagNames, + }); + if (!result.success) { + throw new Error(JSON.stringify(result.errors)); + } + return result.text + .replace( + new RegExp(`<${uniqueKey}_([AQR])>([^<]*)<\\/${uniqueKey}_[AQR]>`, "g"), + (_, type: "A" | "Q" | "R", content: string) => { + return renderers[type](content); + }, + ); +} + +import * as markodown from "@clo/markodown"; +import * as render from "@clo/lib/render"; +import { artifactMap } from "./artifacts.ts"; +import { formatQuestionTimestamp, questionIdToTimestamp } from "./format.ts"; diff --git a/src/q+a/markdown.tsx b/src/q+a/markdown.tsx deleted file mode 100644 index 38e3814b4c190dbafce3c036dbf3eed9fe9132b3..0000000000000000000000000000000000000000 --- a/src/q+a/markdown.tsx +++ /dev/null @@ -1,81 +0,0 @@ -function labelledRedaction(contents: string) { - return ( - - {contents === "name" - ? "old name" - : contents === "username" - ? "old username" - : contents === "domain" - ? "old domain" - : null} - - ); -} - -function questionRef(id: string) { - if (id.length === 12) id = id.slice(0, 10); - const timestamp = questionIdToTimestamp(id); - if (!timestamp) { - console.error(`Invalid question ref: ${id}`); - return [Invalid Question Ref: #{id}]; - } - return ( - - {formatQuestionTimestamp(timestamp)} - - ); -} - -function artifactRef(id: string) { - const artifact = artifactMap[id]; - if (!artifact) { - return [Invalid Artifact Ref: @{id}]; - } - const [title, url, type] = artifact; - if (!url) { - return {title}; - } - return ( - - {title} - - ); -} - -/** classic esbuild trick: use an impossible to guess key for string magic */ -const uniqueKey = crypto.randomUUID().replaceAll("-", ""); - -const renderers = { - A: artifactRef, - Q: questionRef, - R: labelledRedaction, -} as const; - -export function renderMarkdown(source: string) { - const tagNames: markodown.CloverQuestionExtensions = { - question: "q-", - artifactRef: uniqueKey + "_A", - questionRef: uniqueKey + "_Q", - labelledRedaction: uniqueKey + "_R", - }; - const result = markodown.transform({ - source, - markdownOnly: true, - cloverExtensions: tagNames, - }); - if (!result.success) { - throw new Error(JSON.stringify(result.errors)); - } - return result.text - .replace( - new RegExp(`<${uniqueKey}_([AQR])>([^<]*)<\\/${uniqueKey}_[AQR]>`, "g"), - (_, type: "A" | "Q" | "R", content: string) => { - return render.sync(renderers[type](content)).text; - }, - ); -} - -import * as markodown from "@clo/markodown"; -import * as render from "@clo/lib/render"; -import { artifactMap } from "./artifacts.ts"; -import { formatQuestionTimestamp, questionIdToTimestamp } from "./format.ts"; diff --git a/src/q+a/scripts/editor.client.ts b/src/q+a/scripts/editor.client.ts new file mode 100644 index 0000000000000000000000000000000000000000..cbcad92cf4602893e7e5df1d73becac4141b60ef --- /dev/null +++ b/src/q+a/scripts/editor.client.ts @@ -0,0 +1,95 @@ +import type { ScriptPayload } from "#src/q+a/views/editor.marko"; +import QuestionRender from "#src/q+a/tags/question.marko"; + +declare const payload: ScriptPayload; +const date = new Date(payload.date); + +const main = document.getElementById("edit-grid")! as HTMLDivElement; +const preview = document.getElementById("preview")! as HTMLDivElement; +const editor = document.getElementById("editor") as HTMLTextAreaElement; + +async function updatePreview(text: string) { + preview.innerHTML = await QuestionRender.render({ + question: { + id: payload.id, + qmid: payload.qmid, + text: text, + type: payload.type, + date, + }, + editor: true, + }); +} +updatePreview(payload.text); + +editor.value = payload.text; +editor.addEventListener("input", () => { + updatePreview(editor.value); +}); +editor.focus(); + +(globalThis as any).onCommitQuestion = wrapAction(async () => { + const text = editor.value; + const res = await fetch(`/admin/q+a/${payload.id}`, { + method: "PATCH", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text, + type: payload.type, + }), + }); + if (!res.ok) { + throw new Error("Failed to update question, status: " + res.status); + } + if (location.search.includes("return=inbox")) { + location.href = "/admin/q+a/inbox"; + } else { + location.href = "/q+a#q" + payload.id; + } +}); +(globalThis as any).onDelete = wrapAction(async () => { + if (confirm("Are you sure you want to delete this question?")) { + const res = await fetch(`/admin/q+a/${payload.id}`, { + method: "DELETE", + headers: { + Accept: "application/json", + }, + }); + if (!res.ok) { + throw new Error("Failed to delete question, status: " + res.status); + } + location.href = document.referrer || "/admin/q+a"; + } +}); +(globalThis as any).onTypeChange = () => { + payload.type = parseInt( + (document.getElementById("type") as HTMLSelectElement).value, + ); + updatePreview(view.state.doc.toString()); +}; + +function wrapAction(cb: () => Promise) { + return async () => { + main.style.opacity = "0.5"; + main.style.pointerEvents = "none"; + const inputs = main.querySelectorAll( + "button,select,input", + ) as NodeListOf; + inputs.forEach((b) => { + b.disabled = true; + }); + try { + await cb(); + } catch (e: any) { + main.style.opacity = "1"; + main.style.pointerEvents = "auto"; + inputs.forEach((b) => { + b.disabled = false; + }); + alert(e.message); + } + }; +} diff --git a/src/q+a/scripts/editor.client.tsx b/src/q+a/scripts/editor.client.tsx deleted file mode 100644 index cbcad92cf4602893e7e5df1d73becac4141b60ef..0000000000000000000000000000000000000000 --- a/src/q+a/scripts/editor.client.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import type { ScriptPayload } from "#src/q+a/views/editor.marko"; -import QuestionRender from "#src/q+a/tags/question.marko"; - -declare const payload: ScriptPayload; -const date = new Date(payload.date); - -const main = document.getElementById("edit-grid")! as HTMLDivElement; -const preview = document.getElementById("preview")! as HTMLDivElement; -const editor = document.getElementById("editor") as HTMLTextAreaElement; - -async function updatePreview(text: string) { - preview.innerHTML = await QuestionRender.render({ - question: { - id: payload.id, - qmid: payload.qmid, - text: text, - type: payload.type, - date, - }, - editor: true, - }); -} -updatePreview(payload.text); - -editor.value = payload.text; -editor.addEventListener("input", () => { - updatePreview(editor.value); -}); -editor.focus(); - -(globalThis as any).onCommitQuestion = wrapAction(async () => { - const text = editor.value; - const res = await fetch(`/admin/q+a/${payload.id}`, { - method: "PATCH", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - text, - type: payload.type, - }), - }); - if (!res.ok) { - throw new Error("Failed to update question, status: " + res.status); - } - if (location.search.includes("return=inbox")) { - location.href = "/admin/q+a/inbox"; - } else { - location.href = "/q+a#q" + payload.id; - } -}); -(globalThis as any).onDelete = wrapAction(async () => { - if (confirm("Are you sure you want to delete this question?")) { - const res = await fetch(`/admin/q+a/${payload.id}`, { - method: "DELETE", - headers: { - Accept: "application/json", - }, - }); - if (!res.ok) { - throw new Error("Failed to delete question, status: " + res.status); - } - location.href = document.referrer || "/admin/q+a"; - } -}); -(globalThis as any).onTypeChange = () => { - payload.type = parseInt( - (document.getElementById("type") as HTMLSelectElement).value, - ); - updatePreview(view.state.doc.toString()); -}; - -function wrapAction(cb: () => Promise) { - return async () => { - main.style.opacity = "0.5"; - main.style.pointerEvents = "none"; - const inputs = main.querySelectorAll( - "button,select,input", - ) as NodeListOf; - inputs.forEach((b) => { - b.disabled = true; - }); - try { - await cb(); - } catch (e: any) { - main.style.opacity = "1"; - main.style.pointerEvents = "auto"; - inputs.forEach((b) => { - b.disabled = false; - }); - alert(e.message); - } - }; -} diff --git a/src/q+a/tags/question.marko b/src/q+a/tags/question.marko index 12bf3e22e0bcee7c8fb4fa7aac9545e6337251be..f0f22c5b53bd78700e30d4499d873b0852f344bf 100644 --- a/src/q+a/tags/question.marko +++ b/src/q+a/tags/question.marko @@ -39,5 +39,5 @@ export const transitionDate = 1735639200000; import type { Question } from "#src/q+a/models/Question.ts"; import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "#src/q+a/format.ts"; -import * as markdown from "#src/q+a/markdown.tsx"; +import * as markdown from "#src/q+a/markdown.ts"; import { addScript as Script } from "#sitegen" diff --git a/src/q+a/views/editor.marko b/src/q+a/views/editor.marko index e3a2b075dc15e33b11694d29c4161a9d0b4553fc..9e8b0c03b34064651482bf4af763eec4d1db2920 100644 --- a/src/q+a/views/editor.marko +++ b/src/q+a/views/editor.marko @@ -59,9 +59,8 @@ export interface ScriptPayload { -self.payload=${devalue.uneval(payload)} +self.payload=${JSON.stringify(payload).replaceAll('', '<\\/script>')} -import * as devalue from 'devalue'; import { type PendingQuestion } from "#src/q+a/models/PendingQuestion.ts"; import { Question, QuestionType } from "#src/q+a/models/Question.ts";