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",