Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/actions/node-setup/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: "node setup"
description: "node setup"
runs:
using: "composite"
steps:
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # 4.0.1
with:
install_args: "node"
- run: |
corepack enable
corepack prepare --activate
shell: bash
- id: yarn-cache-dir
run: echo "YARN_CACHE_DIR=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
shell: bash
- uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # 5.0.3
with:
path: ${{ steps.yarn-cache-dir.outputs.YARN_CACHE_DIR }}
key: ${{ runner.os }}-node-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-yarn-
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: ci
on:
pull_request:
concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: github.event.head_commit is undefined for pull_request events. The contains() call always returns false, so the condition always evaluates to true and never skips the job.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yaml, line 10:

<comment>`github.event.head_commit` is undefined for `pull_request` events. The `contains()` call always returns `false`, so the condition always evaluates to `true` and never skips the job.</comment>

<file context>
@@ -0,0 +1,31 @@
+jobs:
+  check:
+    runs-on: ubuntu-latest
+    if: "!contains(github.event.head_commit.message, '[skip ci]')"
+    steps:
+      - name: Git checkout
</file context>

steps:
- name: Git checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- name: Setup Node.js
uses: ./.github/actions/node-setup
- run: yarn install --immutable
- name: Run biome
run: yarn check
- name: Run typecheck
run: yarn typecheck
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- name: Git checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
- name: Setup Node.js
uses: ./.github/actions/node-setup
- run: yarn install --immutable
- name: Run build
run: yarn workspaces foreach -Apt run build
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ node_modules

#!.yarn/cache
.pnp.*

# IDE
.idea/
.vscode/

# outputs
dist/
dist-ssr/
.tanstack/
.wvb/
16 changes: 16 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
nodeLinker: node-modules

preferReuse: true

supportedArchitectures:
cpu:
- x64
- ia32
- arm64
libc:
- glibc
- musl
os:
- darwin
- linux
- win32
66 changes: 66 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": [
"**/*.ts",
"**/*.cts",
"**/*.tsx",
"**/*.js",
"**/*.cjs",
"**/*.json",
"**/*.mjs",
"!**/*.js",
"!**/*.cjs",
"!**/*.d.ts",
"!**/*.d.cts",
"!**/routeTree.gen.ts",
"!**/dist",
"!**/dist-ssr"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"trailingNewline": true
},
"linter": {
"enabled": true,
"rules": {
"preset": "recommended",
"suspicious": {
"noExplicitAny": "off",
"noEmptyInterface": "off",
"noArrayIndexKey": "off",
"noConfusingVoidType": "off"
},
"style": {
"noNonNullAssertion": "off",
"noCommonJs": "error"
},
"a11y": {
"noSvgWithoutTitle": "off"
},
"security": {
"noDangerouslySetInnerHtml": "off"
},
"correctness": {
"noUnusedImports": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"arrowParentheses": "asNeeded"
}
}
}
2 changes: 2 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
node = "24.15.0"
15 changes: 14 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
{
"name": "webview-bundle-playground",
"packageManager": "yarn@4.14.1"
"private": true,
"workspaces": [
"webviews/*"
],
"packageManager": "yarn@4.16.0",
"scripts": {
"check": "biome check",
"check:fix": "biome check --write --unsafe",
"typecheck": "yarn workspaces foreach -Apt run typecheck"
},
"devDependencies": {
"@biomejs/biome": "2.5.0",
"typescript": "6.0.3"
}
}
19 changes: 19 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compileOnSave": false,
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": true
}
}
19 changes: 19 additions & 0 deletions webviews/hacker-news/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:," />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#ffffff" />
<title>BUNDLE // news</title>
<meta
name="description"
content="The community for webview-bundle — offline-first web delivery for Electron, Tauri & native webviews."
/>
</head>
<body>
<div id="root"><!--app-html--></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions webviews/hacker-news/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@wvb-playground-webview/hacker-news",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "yarn build:client && yarn build:ssr && yarn prerender",
"build:client": "vite build",
"build:ssr": "vite build --ssr src/entry-server.tsx --outDir dist-ssr --emptyOutDir",
"prerender": "node scripts/prerender.mjs",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@tanstack/react-router": "1.170.15",
"react": "19.2.7",
"react-dom": "19.2.7"
},
"devDependencies": {
"@tailwindcss/vite": "4.3.1",
"@tanstack/router-plugin": "1.168.18",
"@types/react": "^19",
"@types/react-dom": "^19",
"@vitejs/plugin-react": "6.0.2",
"tailwindcss": "4.3.1",
"typescript": "6.0.3",
"vite": "8.0.16"
}
}
37 changes: 37 additions & 0 deletions webviews/hacker-news/scripts/prerender.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Static-site generation step. Runs after the client + SSR builds:
// 1. client build -> dist/ (hashed assets + index.html template)
// 2. ssr build -> dist-ssr/entry-server.js
// 3. this script -> writes dist/<route>/index.html for every known route
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, join, resolve } from 'node:path';
import { getStaticPaths, render } from '../dist-ssr/entry-server.js';

const distDir = resolve(process.cwd(), 'dist');
const template = await readFile(join(distDir, 'index.html'), 'utf8');

function escapeHtml(value) {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}

function buildPage(appHtml, title) {
let html = template;
html = html.includes('<!--app-html-->')
? html.replace('<!--app-html-->', appHtml)
: html.replace(/(<div id="root">)(<\/div>)/, `$1${appHtml}$2`);
html = html.replace(/<title>[\s\S]*?<\/title>/, `<title>${escapeHtml(title)}</title>`);
return html;
}

const paths = getStaticPaths();
for (const { url, title } of paths) {
const appHtml = await render(url);
const outPath = url === '/' ? join(distDir, 'index.html') : join(distDir, url, 'index.html');
await mkdir(dirname(outPath), { recursive: true });
await writeFile(outPath, buildPage(appHtml, title), 'utf8');
}

// SPA fallback for any deep link that wasn't prerendered: ship an empty shell
// that the client boots and routes on its own.
await writeFile(join(distDir, '404.html'), buildPage('', 'BUNDLE // news'), 'utf8');

console.log(`✓ prerendered ${paths.length} routes + 404.html → dist/`);
99 changes: 99 additions & 0 deletions webviews/hacker-news/src/components/CommentTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Link } from '@tanstack/react-router';
import type { FlatComment } from '../data';
import { ageLabel, flattenComments } from '../data';
import { cn } from '../lib/cn';
import { useAppState, useVote } from '../lib/store';

export function CommentTree() {
const { collapsed, toggleCollapse } = useAppState();
const list = flattenComments(collapsed);
return (
<div>
{list.map(c => (
<CommentItem
key={c.id}
c={c}
collapsed={collapsed.has(c.id)}
onToggle={() => toggleCollapse(c.id)}
/>
))}
</div>
);
}

function CommentItem({
c,
collapsed,
onToggle,
}: {
c: FlatComment;
collapsed: boolean;
onToggle: () => void;
}) {
const v = useVote(`c:${c.id}`, c.base);
return (
<div
style={{
marginLeft: c.depth * 16,
paddingLeft: c.depth > 0 ? 12 : 0,
borderLeft: c.depth > 0 ? '1px solid var(--border-1)' : 'none',
}}
>
<div className="flex items-center gap-2 py-[6px] pb-px text-[12px]">
<button
type="button"
onClick={onToggle}
aria-label={collapsed ? 'Expand thread' : 'Collapse thread'}
aria-expanded={!collapsed}
className="inline-block w-[18px] cursor-pointer text-left text-fg-4 transition-colors hover:text-accent"
>
{collapsed ? '[+]' : '[–]'}
</button>
<Link
to="/u/$username"
params={{ username: c.author }}
className="font-semibold text-fg-1 transition-colors hover:text-accent"
>
{c.author}
</Link>
{c.op && (
<span className="rounded-[3px] border border-border-1 bg-accent-subtle px-[5px] text-[9.5px] text-accent">
OP
</span>
)}
<span className="text-fg-4">
{v.score} pts · {ageLabel(c.age)}
</span>
{collapsed && c.childCount > 0 && (
<span className="text-fg-4">· +{c.childCount} hidden</span>
)}
</div>
{!collapsed && (
<>
<div className="pt-0.5 pb-1.5 pl-[26px] text-[13px] leading-[1.55] text-fg-2">
{c.body}
</div>
<div className="flex items-center gap-3 pb-2 pl-[26px] text-[11px] text-fg-4">
<button
type="button"
onClick={v.up}
aria-label="Upvote comment"
className={cn('cursor-pointer transition-colors', v.dir === 1 && 'text-accent')}
>
</button>
<button
type="button"
onClick={v.down}
aria-label="Downvote comment"
className={cn('cursor-pointer transition-colors', v.dir === -1 && 'text-downvote')}
>
</button>
<span className="cursor-pointer transition-colors hover:text-accent">reply</span>
</div>
</>
)}
</div>
);
}
Loading