Compare commits
2 Commits
30cd5c7b13
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e6b128643 | ||
|
|
959f6590c3 |
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
dist
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
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
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -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
|
||||
|
||||
554
package-lock.json
generated
554
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
package.json
17
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HTMLElement>(".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<HTMLElement>(".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);
|
||||
@@ -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<HTMLElement>(".collage-stage");
|
||||
const track = document.querySelector<HTMLElement>(".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);
|
||||
|
||||
20
public/globals.d.ts
vendored
Normal file
20
public/globals.d.ts
vendored
Normal file
@@ -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;
|
||||
};
|
||||
}
|
||||
4
scripts/copy-assets.mjs
Normal file
4
scripts/copy-assets.mjs
Normal file
@@ -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");
|
||||
@@ -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();
|
||||
@@ -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 = {
|
||||
@@ -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";
|
||||
@@ -399,7 +399,7 @@ export function casparTemplatePage() {
|
||||
<main class="stage fit-contain">
|
||||
<section class="embed provider-empty" data-source="" style="--embed-width:500px"></section>
|
||||
</main>
|
||||
<script src="/caspar.js"></script>
|
||||
<script type="module" src="/caspar.js"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -477,7 +477,7 @@ export function collagePage({
|
||||
<div class="collage-track"></div>
|
||||
</main>
|
||||
<script id="collage-data" type="application/json">${safeJson(collageData)}</script>
|
||||
<script src="/collage.js"></script>
|
||||
<script type="module" src="/collage.js"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -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*");
|
||||
|
||||
@@ -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('<iframe src="https://player.example.com/video?id=1"></iframe>');
|
||||
@@ -207,7 +207,7 @@ test("renders a CasparCG template shell", () => {
|
||||
|
||||
assert.match(page, /<body class="graphic transparent is-loading"/);
|
||||
assert.match(page, /<section class="embed provider-empty"/);
|
||||
assert.match(page, /<script src="\/caspar\.js"><\/script>/);
|
||||
assert.match(page, /<script type="module" src="\/caspar\.js"><\/script>/);
|
||||
});
|
||||
|
||||
test("renders a vertical scrolling collage page with repeated embed cards", () => {
|
||||
@@ -261,7 +261,7 @@ test("renders a vertical scrolling collage page with repeated embed cards", () =
|
||||
assert.match(page, /"hydrateDelay":180/);
|
||||
assert.match(page, /"repeat":3/);
|
||||
assert.match(page, /"shuffle":false/);
|
||||
assert.match(page, /<script src="\/collage\.js"><\/script>/);
|
||||
assert.match(page, /<script type="module" src="\/collage\.js"><\/script>/);
|
||||
});
|
||||
|
||||
test("shrinks collage card width to fit the configured screen width", () => {
|
||||
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
Reference in New Issue
Block a user