From 959f6590c39f4080161936e9ca8ada9a1c0a5776 Mon Sep 17 00:00:00 2001 From: Aiden Wilson <68633820+awils27@users.noreply.github.com> Date: Fri, 29 May 2026 23:55:31 +1000 Subject: [PATCH] conversion to type script --- .gitignore | 1 + Dockerfile | 7 +- README.md | 3 + package-lock.json | 554 +++++++++++++++++++++++++++++ package.json | 17 +- public/{caspar.js => caspar.ts} | 10 +- public/{collage.js => collage.ts} | 12 +- public/globals.d.ts | 20 ++ scripts/copy-assets.mjs | 4 + src/{oembed.js => oembed.ts} | 44 ++- src/{providers.js => providers.ts} | 40 ++- src/{server.js => server.ts} | 4 +- src/{templates.js => templates.ts} | 0 test/providers.test.js | 4 +- test/templates.test.js | 2 +- tsconfig.json | 17 + 16 files changed, 695 insertions(+), 44 deletions(-) rename public/{caspar.js => caspar.ts} (97%) rename public/{collage.js => collage.ts} (95%) create mode 100644 public/globals.d.ts create mode 100644 scripts/copy-assets.mjs rename src/{oembed.js => oembed.ts} (60%) rename src/{providers.js => providers.ts} (70%) rename src/{server.js => server.ts} (97%) rename src/{templates.js => templates.ts} (100%) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 2309cc8..cdc5c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage +dist *.lcov # nyc test coverage diff --git a/Dockerfile b/Dockerfile index 7399442..a47263d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,13 @@ ENV NODE_ENV=production WORKDIR /app COPY package*.json ./ -RUN npm ci --omit=dev +RUN npm ci -COPY public ./public COPY src ./src +COPY public ./public +COPY scripts ./scripts +COPY tsconfig.json ./ +RUN npm run build && npm prune --omit=dev EXPOSE 3000 CMD ["npm", "start"] diff --git a/README.md b/README.md index 7c9f527..ec57ca7 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,14 @@ OBS Browser Source, OGraf, or any HTML-capable character generator. ```sh npm install +npm run build npm start ``` Open `http://localhost:3000`. +For development, use `npm run dev` to run the TypeScript server in watch mode. + ## Run with Docker ```sh diff --git a/package-lock.json b/package-lock.json index 9264408..9af7d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,563 @@ "name": "oembed-graphics", "version": "0.1.0", "license": "AGPL-3.0-or-later", + "devDependencies": { + "@types/node": "^25.9.1", + "tsx": "^4.22.3", + "typescript": "^6.0.3" + }, "engines": { "node": ">=20" } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 1cf37a2..437895b 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,19 @@ "description": "Docker-hosted oEmbed graphics service for broadcast overlays.", "type": "module", "scripts": { - "dev": "node --watch src/server.js", - "start": "node src/server.js", - "typecheck": "node --check src/server.js && node --check src/oembed.js && node --check src/providers.js && node --check src/templates.js && node --check public/caspar.js && node --check public/collage.js", - "build": "npm run typecheck", - "test": "node --test" + "dev": "npm run build && tsx watch src/server.ts", + "start": "node dist/src/server.js", + "typecheck": "tsc --noEmit", + "build": "tsc && node scripts/copy-assets.mjs", + "test": "npm run build && node --test" }, "engines": { "node": ">=20" }, - "license": "AGPL-3.0-or-later" + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@types/node": "^25.9.1", + "tsx": "^4.22.3", + "typescript": "^6.0.3" + } } diff --git a/public/caspar.js b/public/caspar.ts similarity index 97% rename from public/caspar.js rename to public/caspar.ts index a381962..31c7af7 100644 --- a/public/caspar.js +++ b/public/caspar.ts @@ -202,7 +202,10 @@ function applyStageOptions(options) { document.body.classList.toggle("transparent", options.transparent); document.documentElement.classList.toggle("transparent", options.transparent); - const stage = document.querySelector(".stage"); + const stage = document.querySelector(".stage"); + if (!stage) { + return; + } stage.classList.toggle("fit-cover", options.fit === "cover"); stage.classList.toggle("fit-contain", options.fit !== "cover"); } @@ -242,7 +245,10 @@ function startAutoplayAssist(root, options) { } function renderEmbed(oembed, sourceUrl, options) { - const embed = document.querySelector(".embed"); + const embed = document.querySelector(".embed"); + if (!embed) { + return; + } const providerClass = `provider-${slugify(oembed.provider_name)}`; const embedWidth = Math.min(Number(oembed.width) || 500, 500); const embedHeight = Number(oembed.height); diff --git a/public/collage.js b/public/collage.ts similarity index 95% rename from public/collage.js rename to public/collage.ts index ef6c7e9..9f1119f 100644 --- a/public/collage.js +++ b/public/collage.ts @@ -1,8 +1,12 @@ -const dataElement = document.getElementById("collage-data"); -const stage = document.querySelector(".collage-stage"); -const track = document.querySelector(".collage-track"); +const dataElement = document.getElementById("collage-data") as HTMLScriptElement; +const stage = document.querySelector(".collage-stage"); +const track = document.querySelector(".collage-track"); const config = JSON.parse(dataElement.textContent); +if (!stage || !track) { + throw new Error("Collage stage could not be initialized."); +} + let lastFrame = performance.now(); let itemCursor = 0; const columns = []; @@ -15,7 +19,7 @@ let trackTop = 0; let hydrationTimer; const cardObserver = new ResizeObserver((entries) => { for (const entry of entries) { - const card = entry.target; + const card = entry.target as HTMLElement; const column = cardColumns.get(card); const previousExtent = cardExtent(card); diff --git a/public/globals.d.ts b/public/globals.d.ts new file mode 100644 index 0000000..e5c7cbe --- /dev/null +++ b/public/globals.d.ts @@ -0,0 +1,20 @@ +type ProviderWidgetRoot = Element | Document; + +interface Window { + update?: (payload: unknown) => void; + play?: () => void; + stop?: () => void; + twttr?: { + widgets?: { + load?: (root?: ProviderWidgetRoot) => void; + }; + }; + instgrm?: { + Embeds?: { + process?: () => void; + }; + }; + bluesky?: { + scan?: (root?: ProviderWidgetRoot) => void; + }; +} diff --git a/scripts/copy-assets.mjs b/scripts/copy-assets.mjs new file mode 100644 index 0000000..d89e8fc --- /dev/null +++ b/scripts/copy-assets.mjs @@ -0,0 +1,4 @@ +import { cp, mkdir } from "node:fs/promises"; + +await mkdir("dist/public", { recursive: true }); +await cp("public/styles.css", "dist/public/styles.css"); diff --git a/src/oembed.js b/src/oembed.ts similarity index 60% rename from src/oembed.js rename to src/oembed.ts index 197911c..2a7b73c 100644 --- a/src/oembed.js +++ b/src/oembed.ts @@ -1,9 +1,27 @@ -import { findProvider, loadProviders } from "./providers.js"; +import { findProvider, loadProviders, type Provider } from "./providers.js"; const DEFAULT_TIMEOUT_MS = 8000; const DEFAULT_MAX_WIDTH = 500; -function normalizeMaxWidth(endpoint, maxWidth) { +type HttpError = Error & { status?: number }; + +type FetchImpl = typeof fetch; + +type FetchOembedOptions = { + fetchImpl?: FetchImpl; + providers?: Provider[]; + timeoutMs?: number; + maxWidth?: number; + maxHeight?: number; +}; + +function httpError(message: string, status: number): HttpError { + const error: HttpError = new Error(message); + error.status = status; + return error; +} + +function normalizeMaxWidth(_endpoint: string, maxWidth?: number) { if (!maxWidth) { return maxWidth; } @@ -11,7 +29,7 @@ function normalizeMaxWidth(endpoint, maxWidth) { return Math.min(maxWidth, DEFAULT_MAX_WIDTH); } -export function buildOembedUrl(endpoint, targetUrl, maxWidth, maxHeight) { +export function buildOembedUrl(endpoint: string, targetUrl: string, maxWidth?: number, maxHeight?: number) { const requestUrl = new URL(endpoint); const normalizedMaxWidth = normalizeMaxWidth(endpoint, maxWidth); requestUrl.searchParams.set("url", targetUrl); @@ -28,36 +46,30 @@ export function buildOembedUrl(endpoint, targetUrl, maxWidth, maxHeight) { return requestUrl; } -export async function fetchOembed(targetUrl, { +export async function fetchOembed(targetUrl: string, { fetchImpl = fetch, providers, timeoutMs = Number(process.env.OEMBED_TIMEOUT_MS || DEFAULT_TIMEOUT_MS), maxWidth, maxHeight, -} = {}) { +}: FetchOembedOptions = {}) { let parsed; try { parsed = new URL(targetUrl); } catch { - const error = new Error("The url parameter must be an absolute URL."); - error.status = 400; - throw error; + throw httpError("The url parameter must be an absolute URL.", 400); } if (!["http:", "https:"].includes(parsed.protocol)) { - const error = new Error("Only http and https URLs are supported."); - error.status = 400; - throw error; + throw httpError("Only http and https URLs are supported.", 400); } const availableProviders = providers || await loadProviders({ fetchImpl }); const provider = findProvider(targetUrl, availableProviders); if (!provider) { - const error = new Error("No oEmbed provider matched this URL."); - error.status = 404; - throw error; + throw httpError("No oEmbed provider matched this URL.", 404); } const controller = new AbortController(); @@ -73,9 +85,7 @@ export async function fetchOembed(targetUrl, { }); if (!response.ok) { - const error = new Error(`oEmbed provider returned ${response.status}.`); - error.status = response.status; - throw error; + throw httpError(`oEmbed provider returned ${response.status}.`, response.status); } const data = await response.json(); diff --git a/src/providers.js b/src/providers.ts similarity index 70% rename from src/providers.js rename to src/providers.ts index e778a03..2e7b5e2 100644 --- a/src/providers.js +++ b/src/providers.ts @@ -1,22 +1,39 @@ const DEFAULT_PROVIDERS_URL = "https://oembed.com/providers.json"; const DEFAULT_TTL_MS = 12 * 60 * 60 * 1000; -let providerCache = { - expiresAt: 0, - providers: [], +export type RawProvider = { + provider_name: string; + provider_url: string; + endpoints?: Array<{ + schemes?: string[]; + url: string; + }>; }; -export function wildcardToRegExp(pattern) { +export type Provider = { + providerName: string; + providerUrl: string; + endpoint: string; + scheme: string; + regex: RegExp; +}; + +let providerCache = { + expiresAt: 0, + providers: [] as Provider[], +}; + +export function wildcardToRegExp(pattern: string) { const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); return new RegExp(`^${escaped}$`, "i"); } -export function normalizeEndpoint(endpoint) { +export function normalizeEndpoint(endpoint: string) { return endpoint.replace("{format}", "json"); } -export function flattenProviders(rawProviders) { - const providers = []; +export function flattenProviders(rawProviders: RawProvider[]) { + const providers: Provider[] = []; for (const provider of rawProviders) { for (const endpoint of provider.endpoints || []) { @@ -37,7 +54,7 @@ export function flattenProviders(rawProviders) { return providers; } -export function findProvider(url, providers) { +export function findProvider(url: string, providers: Provider[]) { return providers.find((provider) => provider.regex.test(url)); } @@ -46,6 +63,11 @@ export async function loadProviders({ ttlMs = Number(process.env.PROVIDERS_TTL_MS || DEFAULT_TTL_MS), fetchImpl = fetch, force = false, +}: { + providersUrl?: string; + ttlMs?: number; + fetchImpl?: typeof fetch; + force?: boolean; } = {}) { const now = Date.now(); @@ -64,7 +86,7 @@ export async function loadProviders({ throw new Error(`Could not load providers: ${response.status} ${response.statusText}`); } - const rawProviders = await response.json(); + const rawProviders = await response.json() as RawProvider[]; const providers = flattenProviders(rawProviders); providerCache = { diff --git a/src/server.js b/src/server.ts similarity index 97% rename from src/server.js rename to src/server.ts index c82a393..c3f1cb3 100644 --- a/src/server.js +++ b/src/server.ts @@ -1,4 +1,5 @@ import { createServer } from "node:http"; +import { existsSync } from "node:fs"; import { readFile } from "node:fs/promises"; import { extname, join } from "node:path"; import { fileURLToPath } from "node:url"; @@ -9,7 +10,8 @@ import { casparTemplatePage, collagePage, errorPage, graphicPage, homePage } fro const __dirname = fileURLToPath(new URL(".", import.meta.url)); const rootDir = join(__dirname, ".."); -const publicDir = join(rootDir, "public"); +const compiledPublicDir = join(rootDir, "dist", "public"); +const publicDir = existsSync(compiledPublicDir) ? compiledPublicDir : join(rootDir, "public"); const port = Number(process.env.PORT || 3000); const host = process.env.HOST || "0.0.0.0"; diff --git a/src/templates.js b/src/templates.ts similarity index 100% rename from src/templates.js rename to src/templates.ts diff --git a/test/providers.test.js b/test/providers.test.js index c85ddf1..9b69f92 100644 --- a/test/providers.test.js +++ b/test/providers.test.js @@ -1,8 +1,8 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { buildOembedUrl } from "../src/oembed.js"; -import { findProvider, flattenProviders, wildcardToRegExp } from "../src/providers.js"; +import { buildOembedUrl } from "../dist/src/oembed.js"; +import { findProvider, flattenProviders, wildcardToRegExp } from "../dist/src/providers.js"; test("wildcard provider schemes match target URLs", () => { const regex = wildcardToRegExp("https://*.example.com/watch*"); diff --git a/test/templates.test.js b/test/templates.test.js index 2bbd07a..a2b684c 100644 --- a/test/templates.test.js +++ b/test/templates.test.js @@ -1,7 +1,7 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { casparTemplatePage, collagePage, graphicPage, homePage, prepareEmbedHtml } from "../src/templates.js"; +import { casparTemplatePage, collagePage, graphicPage, homePage, prepareEmbedHtml } from "../dist/src/templates.js"; test("adds autoplay parameters and permissions to iframe embeds", () => { const html = prepareEmbedHtml(''); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a9ed6e4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node"], + "rootDir": ".", + "outDir": "dist", + "strict": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "public/**/*.ts"], + "exclude": ["dist", "node_modules"] +}