diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8ac7140..b4ec835 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -102,10 +102,6 @@ jobs:
workspaces: packages/stream
- run: npm ci
- run: npm run build
- # The same page as a Samsung TV package, unsigned: a set only installs a package signed
- # for its own DUID, so each owner signs their copy (Apps2Samsung, or the CLI route on the
- # install page), and this job needs no Tizen SDK. The wasm is the one `npm run build` made.
- - run: npm run build:tizen -w punktfunk-web
# The packed library, installed into an empty Vite project: the consumer's view.
- run: ./scripts/pack-gate.sh
- uses: actions/upload-artifact@v7
@@ -116,12 +112,6 @@ jobs:
with:
name: punktfunk-web
path: apps/web/dist/
- - uses: actions/upload-artifact@v7
- with:
- name: punktfunk-tizen
- path: apps/web/punktfunk-tizen-*.wgt
- # The `.wgt` rides the release beside the tarball; its name is what the Apps2Samsung
- # catalog entry matches (`^punktfunk-tizen-.*\.wgt$`).
- name: Release
if: startsWith(github.ref, 'refs/tags/v')
env:
@@ -129,7 +119,7 @@ jobs:
run: |
tarball="punktfunk-client-web-${GITHUB_REF_NAME}.tar.gz"
tar -czf "$tarball" -C apps/web/dist .
- gh release create "$GITHUB_REF_NAME" "$tarball" apps/web/punktfunk-tizen-*.wgt --verify-tag --generate-notes
+ gh release create "$GITHUB_REF_NAME" "$tarball" --verify-tag --generate-notes
# The container: the server compiled natively on each architecture's runner, the client from the
# build job. Pushed to git.unom.io from main (`edge`) and from v* tags (`X.Y.Z` and `latest`);
diff --git a/.gitignore b/.gitignore
index 30e952f..57383f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,8 +5,6 @@ node_modules/
/packages/stream/wasm/*.js
/packages/stream/wasm/*.wasm
/apps/web/dist
-/apps/web/dist-tizen
-/apps/web/*.wgt
/apps/web/storybook-static
/apps/web/screenshots
/target
diff --git a/apps/web/package.json b/apps/web/package.json
index b3ee595..842ff6f 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -7,7 +7,6 @@
"scripts": {
"dev": "vite",
"build": "vite build",
- "build:tizen": "vite build --mode tizen && node tools/wgt.mjs",
"check": "tsc --noEmit",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
diff --git a/apps/web/tizen/config.xml b/apps/web/tizen/config.xml
deleted file mode 100644
index a391975..0000000
--- a/apps/web/tizen/config.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
-
-
- punktfunk
-
-
-
-
-
-
-
-
diff --git a/apps/web/tizen/icon.png b/apps/web/tizen/icon.png
deleted file mode 100644
index 03275cc..0000000
Binary files a/apps/web/tizen/icon.png and /dev/null differ
diff --git a/apps/web/tools/tizen-icon.mjs b/apps/web/tools/tizen-icon.mjs
deleted file mode 100644
index d57dcd1..0000000
--- a/apps/web/tools/tizen-icon.mjs
+++ /dev/null
@@ -1,29 +0,0 @@
-// The Samsung TV package's icon, 512×423 as the catalog wants it: the lens mark from
-// `src/ui/brand.tsx` on the page's own ground, rendered once by Chromium and committed as
-// `tizen/icon.png`. Run again only when the mark changes:
-//
-// node tools/tizen-icon.mjs (Playwright's Chromium; `npx playwright install chromium`)
-
-import { chromium } from "playwright";
-import { fileURLToPath } from "node:url";
-
-const LIGHT =
- "M403.037,791.672c107.586,0 194.41,-86.824 194.41,-194.41c0,-107.586 -86.824,-194.41 -194.41,-194.41c-107.586,0 -194.41,86.824 -194.41,194.41c0,107.586 86.824,194.41 194.41,194.41Z";
-const DEEP =
- "M735.276,540.321c76.075,-76.075 76.075,-198.862 0,-274.937c-76.075,-76.075 -198.862,-76.075 -274.937,0c-76.075,76.075 -76.075,198.862 0,274.937c76.075,76.075 198.862,76.075 274.937,0Z";
-const OVERLAP =
- "M647.84,590.737c-64.853,17.403 -136.871,0.597 -187.885,-50.416c-51.013,-51.013 -67.819,-123.032 -50.416,-187.885c64.853,-17.403 136.871,-0.597 187.885,50.416c51.013,51.013 67.819,123.032 50.416,187.885Z";
-
-const html = `
-`;
-
-const out = fileURLToPath(new URL("../tizen/icon.png", import.meta.url));
-const executablePath = process.env["PF_CHROMIUM"];
-const browser = await chromium.launch(executablePath ? { executablePath } : {});
-const page = await browser.newPage({ viewport: { width: 512, height: 423 }, deviceScaleFactor: 1 });
-await page.setContent(html);
-await page.screenshot({ path: out, clip: { x: 0, y: 0, width: 512, height: 423 } });
-await browser.close();
-console.log(`icon: ${out}`);
diff --git a/apps/web/tools/wgt.mjs b/apps/web/tools/wgt.mjs
deleted file mode 100644
index a533427..0000000
--- a/apps/web/tools/wgt.mjs
+++ /dev/null
@@ -1,116 +0,0 @@
-// `dist-tizen/` as a `.wgt`: a zip with `config.xml` at its root, which is all a Tizen widget
-// package is before it is signed. Unsigned on purpose — a set from 2023 on only installs a
-// package signed for its own DUID, so each owner signs their copy (Apps2Samsung, or the CLI route
-// on the install page), and CI needs no Tizen SDK to publish one.
-//
-// A zip writer of its own rather than a dependency: the format is a local header, the deflated
-// bytes, and a central directory, and node ships the deflate.
-//
-// node tools/wgt.mjs → apps/web/punktfunk-tizen-.wgt
-
-import { deflateRawSync } from "node:zlib";
-import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
-import { join, relative, resolve, sep } from "node:path";
-import { fileURLToPath } from "node:url";
-
-const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
-const dist = join(root, "dist-tizen");
-
-/** Every file under `dir`, as `[zip path, absolute path]`, the manifest first. */
-function files(dir) {
- const out = [];
- const walk = (d) => {
- for (const name of readdirSync(d).sort()) {
- const p = join(d, name);
- if (statSync(p).isDirectory()) walk(p);
- else out.push([relative(dir, p).split(sep).join("/"), p]);
- }
- };
- walk(dir);
- out.sort(([a], [b]) => (a === "config.xml" ? -1 : b === "config.xml" ? 1 : a < b ? -1 : 1));
- return out;
-}
-
-const CRC = new Uint32Array(256).map((_, n) => {
- let c = n;
- for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
- return c >>> 0;
-});
-
-function crc32(bytes) {
- let c = 0xffffffff;
- for (const b of bytes) c = CRC[(c ^ b) & 0xff] ^ (c >>> 8);
- return (c ^ 0xffffffff) >>> 0;
-}
-
-/** A zip of `entries` (`[path, bytes]`), deflated, with a fixed timestamp so a rebuild of the
- * same files is the same archive byte for byte. */
-export function zip(entries) {
- const locals = [];
- const central = [];
- let offset = 0;
- const date = (1 << 5) | 1; // 1980-01-01 in DOS date form
- const time = 0;
- for (const [path, data] of entries) {
- const name = Buffer.from(path, "utf8");
- const packed = deflateRawSync(data);
- const crc = crc32(data);
- const local = Buffer.alloc(30);
- local.writeUInt32LE(0x04034b50, 0);
- local.writeUInt16LE(20, 4); // version needed
- local.writeUInt16LE(0x0800, 6); // utf-8 names
- local.writeUInt16LE(8, 8); // deflate
- local.writeUInt16LE(time, 10);
- local.writeUInt16LE(date, 12);
- local.writeUInt32LE(crc, 14);
- local.writeUInt32LE(packed.length, 18);
- local.writeUInt32LE(data.length, 22);
- local.writeUInt16LE(name.length, 26);
- local.writeUInt16LE(0, 28);
- locals.push(local, name, packed);
- const entry = Buffer.alloc(46);
- entry.writeUInt32LE(0x02014b50, 0);
- entry.writeUInt16LE(20, 4); // made by
- entry.writeUInt16LE(20, 6); // version needed
- entry.writeUInt16LE(0x0800, 8);
- entry.writeUInt16LE(8, 10);
- entry.writeUInt16LE(time, 12);
- entry.writeUInt16LE(date, 14);
- entry.writeUInt32LE(crc, 16);
- entry.writeUInt32LE(packed.length, 20);
- entry.writeUInt32LE(data.length, 24);
- entry.writeUInt16LE(name.length, 28);
- entry.writeUInt16LE(0, 30); // extra
- entry.writeUInt16LE(0, 32); // comment
- entry.writeUInt16LE(0, 34); // disk
- entry.writeUInt16LE(0, 36); // internal attrs
- entry.writeUInt32LE(0, 38); // external attrs
- entry.writeUInt32LE(offset, 42);
- central.push(entry, name);
- offset += local.length + name.length + packed.length;
- }
- const dirBytes = central.reduce((n, b) => n + b.length, 0);
- const end = Buffer.alloc(22);
- end.writeUInt32LE(0x06054b50, 0);
- end.writeUInt16LE(0, 4);
- end.writeUInt16LE(0, 6);
- end.writeUInt16LE(entries.length, 8);
- end.writeUInt16LE(entries.length, 10);
- end.writeUInt32LE(dirBytes, 12);
- end.writeUInt32LE(offset, 16);
- end.writeUInt16LE(0, 20);
- return Buffer.concat([...locals, ...central, end]);
-}
-
-if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
- const list = files(dist);
- const config = list.find(([p]) => p === "config.xml");
- if (!config) {
- console.error(`wgt: ${dist} has no config.xml — run \`vite build --mode tizen\` first`);
- process.exit(1);
- }
- const version = /version="([^"]+)"/.exec(readFileSync(config[1], "utf8"))?.[1] ?? "0.0.0";
- const out = join(root, `punktfunk-tizen-${version}.wgt`);
- writeFileSync(out, zip(list.map(([p, abs]) => [p, readFileSync(abs)])));
- console.log(`wgt: ${relative(process.cwd(), out)} (${list.length} files, unsigned)`);
-}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index 0451621..88e004d 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,17 +1,12 @@
// The app's bundler. The wasm module is `@punktfunk/stream`'s concern: the engine imports
// emscripten's glue lazily, and the glue finds its `.wasm` through `new URL(…, import.meta.url)`,
// which Vite turns into an emitted asset. Nothing here names either file.
-//
-// `--mode tizen` builds the same page as a Samsung TV package: `dist-tizen/` with the widget's
-// `config.xml` and icon beside `index.html`, no source maps, and Samsung's `webapis.js` on the
-// page. `tools/wgt.mjs` zips that directory into the `.wgt`.
import { execFileSync } from "node:child_process";
-import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
-import { defineConfig, type Plugin } from "vite";
+import { defineConfig } from "vite";
const devHost = process.env["PF_HOST"];
@@ -24,80 +19,46 @@ function version(): string {
}
}
-/** The numeric part of the version, which is all a widget's `version` may hold: `0.3.0` from
- * `v0.3.0-12-gabcdef`, and `0.0.1` for a build with no tag behind it. */
-function widgetVersion(described: string = version()): string {
- return /\d+\.\d+\.\d+/.exec(described)?.[0] ?? "0.0.1";
-}
-
-/** The package's own files beside the page: `config.xml` at this build's version, the icon, and
- * Samsung's `webapis.js`, whose path exists only on the set and so is never bundled. */
-function tizenPackage(): Plugin {
- const here = (p: string) => fileURLToPath(new URL(p, import.meta.url));
- return {
- name: "punktfunk-tizen",
- transformIndexHtml: {
- order: "post",
- handler: (html) => ({
- html,
- tags: [{ tag: "script", attrs: { src: "$WEBAPIS/webapis/webapis.js" }, injectTo: "head-prepend" }],
- }),
- },
- generateBundle() {
- const config = readFileSync(here("./tizen/config.xml"), "utf8").replace(
- /version="[^"]*"/,
- `version="${widgetVersion()}"`,
- );
- this.emitFile({ type: "asset", fileName: "config.xml", source: config });
- this.emitFile({ type: "asset", fileName: "icon.png", source: readFileSync(here("./tizen/icon.png")) });
- },
- };
-}
-
-export default defineConfig(({ mode }) => {
- const tizen = mode === "tizen";
- return {
- // Relative asset URLs, so the same `dist/` works at a host's root, under a path, or packaged.
- base: "./",
- plugins: [react(), tailwindcss(), ...(tizen ? [tizenPackage()] : [])],
- // Through the dev proxy the API answers on the page's origin but the WebTransport plane does
- // not; this tells the engine where it is. Undefined in a real build, so the address the user
- // typed is used — exactly as designed.
- define: {
- __PF_VERSION__: JSON.stringify(version()),
- __PF_TRANSPORT_HOST__: JSON.stringify(devHost ? new URL(devHost).hostname : undefined),
- },
- resolve: {
- alias: {
- // The library from source, as `tsconfig.json` does for the types: the workspace's own
- // consumer follows the engine without a build in between.
- "@punktfunk/stream": fileURLToPath(new URL("../../packages/stream/src/index.ts", import.meta.url)),
- "@": fileURLToPath(new URL("./src", import.meta.url)),
- },
- },
- build: {
- outDir: tizen ? "dist-tizen" : "dist",
- emptyOutDir: true,
- // What the wasm and WebCodecs already require; nothing older can run this page anyway.
- target: "es2022",
- // A package ships to a set with no devtools to read a map, and the maps are half its size.
- sourcemap: !tizen,
- },
- server: {
- port: 5173,
- strictPort: true,
- // Development against a real host without accepting its certificate first: `PF_HOST` names
- // it, and the dev server answers `/api` on the page's own origin. Only the dev server does
- // this — a built page talks to the host it was pointed at, cross-origin, as designed.
- ...(devHost
- ? {
- proxy: {
- "/api": { target: devHost, changeOrigin: true, secure: false },
- // The end-to-end harness posts its findings here; a collector listens during a run.
- "/report": { target: "http://127.0.0.1:8099" },
- },
- }
- : {}),
+export default defineConfig({
+ // Relative asset URLs, so the same `dist/` works at a host's root, under a path, or packaged.
+ base: "./",
+ plugins: [react(), tailwindcss()],
+ // Through the dev proxy the API answers on the page's origin but the WebTransport plane does
+ // not; this tells the engine where it is. Undefined in a real build, so the address the user
+ // typed is used — exactly as designed.
+ define: {
+ __PF_VERSION__: JSON.stringify(version()),
+ __PF_TRANSPORT_HOST__: JSON.stringify(devHost ? new URL(devHost).hostname : undefined),
+ },
+ resolve: {
+ alias: {
+ // The library from source, as `tsconfig.json` does for the types: the workspace's own
+ // consumer follows the engine without a build in between.
+ "@punktfunk/stream": fileURLToPath(new URL("../../packages/stream/src/index.ts", import.meta.url)),
+ "@": fileURLToPath(new URL("./src", import.meta.url)),
},
- };
+ },
+ build: {
+ outDir: "dist",
+ emptyOutDir: true,
+ // What the wasm and WebCodecs already require; nothing older can run this page anyway.
+ target: "es2022",
+ sourcemap: true,
+ },
+ server: {
+ port: 5173,
+ strictPort: true,
+ // Development against a real host without accepting its certificate first: `PF_HOST` names
+ // it, and the dev server answers `/api` on the page's own origin. Only the dev server does
+ // this — a built page talks to the host it was pointed at, cross-origin, as designed.
+ ...(devHost
+ ? {
+ proxy: {
+ "/api": { target: devHost, changeOrigin: true, secure: false },
+ // The end-to-end harness posts its findings here; a collector listens during a run.
+ "/report": { target: "http://127.0.0.1:8099" },
+ },
+ }
+ : {}),
+ },
});
diff --git a/package.json b/package.json
index 2f2164a..1302921 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,6 @@
],
"scripts": {
"build": "npm run build -w @punktfunk/stream && npm run build -w punktfunk-web",
- "build:tizen": "npm run build -w @punktfunk/stream && npm run build:tizen -w punktfunk-web",
"check": "npm run check --workspaces",
"test": "npm run test --workspaces --if-present",
"dev": "npm run dev -w punktfunk-web",