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
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,24 @@ jobs:
- name: Lint
run: npm run lint

knip:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Find unused dependencies
run: npm run knip

playwright-version:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repository.
- `npm run preview` - Preview production build locally
- `npm run format` - Format all files with Oxfmt
- `npm run typecheck` - Check TypeScript types
- `npm run knip` - Find unused or missing dependencies

## Code Quality

Expand Down
4 changes: 4 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["scripts/**/*.{js,ts,mjs,cjs}"]
}
7,900 changes: 2,028 additions & 5,872 deletions package-lock.json

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,43 +20,40 @@
"test:ui": "vitest --ui",
"format": "oxfmt",
"format:check": "oxfmt --check",
"qa": "concurrently --group --names 'format,typecheck,lint,test' 'npm run format:check' 'npm run typecheck' 'npm run lint' 'npm run test'",
"knip": "knip",
"qa": "concurrently --group --names 'format,typecheck,lint,knip,test' 'npm run format:check' 'npm run typecheck' 'npm run lint' 'npm run knip' 'npm run test'",
"convert:webp": "node --experimental-strip-types scripts/convert-to-webp.ts",
"convert:webp:dry": "node --experimental-strip-types scripts/convert-to-webp.ts --dry-run"
},
"dependencies": {
"@vercel/analytics": "2.0.1",
"@xstate/react": "6.1.0",
"array-shuffle": "4.1.0",
"clsx": "2.1.1",
"howler": "2.2.4",
"motion": "13.2.0",
"react": "19.2.8",
"react-dom": "19.2.8",
"reset-css": "5.0.2",
"xstate": "5.32.6"
},
"devDependencies": {
"@types/howler": "2.2.13",
"@types/node": "26.4.1",
"@types/random": "3.0.1",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.7",
"@vitejs/plugin-react": "6.1.1",
"@vitest/browser": "^5.0.0",
"@vitest/browser-playwright": "^5.0.0",
"@vitest/ui": "^5.0.0",
"concurrently": "^10.0.5",
"knip": "^6.34.0",
"oxfmt": "^0.66.0",
"oxlint": "^1.80.0",
"oxlint-tsgolint": "^7.0.2001",
"playwright": "^1.62.1",
"semver": "^7.8.5",
"sharp": "^0.35.4",
"typescript": "7.0.2",
"vite": "8.2.2",
"vite-imagetools": "^12.0.1",
"vite-plugin-compression": "0.5.1",
"vite-plugin-imagemin": "^0.6.1",
"vitest": "^5.0.0",
"vitest-browser-react": "^2.3.0"
},
Expand Down
24 changes: 13 additions & 11 deletions scripts/convert-to-webp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,16 +161,19 @@ async function main() {
const exclusions = [...defaultExclusions, ...options.exclude]
const startDir = process.cwd()

console.log('🔍 Scanning for images...\n')
if (options.verbose) {
console.log('🔍 Scanning for images...')
}

if (options.dryRun) {
console.log('🏃 DRY RUN MODE - No files will be modified\n')
console.log('🏃 DRY RUN MODE - No files will be modified')
}

let totalOriginalSize = 0
let totalWebpSize = 0
let convertedCount = 0
let skippedCount = 0
let wroteProgress = false

for await (const imagePath of findImages(startDir, exclusions)) {
const result = await convertImage(imagePath, options)
Expand All @@ -179,21 +182,22 @@ async function main() {
totalOriginalSize += result.originalSize
totalWebpSize += result.webpSize
convertedCount++
if (!options.verbose) wroteProgress = true
} else if (!options.dryRun) {
skippedCount++
}
}

if (!options.verbose && !options.dryRun) {
console.log('') // New line after progress dots
// Progress dots are written without newlines; finish the line only if we wrote any.
if (wroteProgress) {
process.stdout.write('\n')
}

console.log('\n📊 Summary:')
console.log(` Converted: ${convertedCount} images`)

const summaryParts = [`converted ${convertedCount}`]
if (skippedCount > 0) {
console.log(` Skipped: ${skippedCount} images (WebP already exists)`)
summaryParts.push(`skipped ${skippedCount} (already exist)`)
}
console.log(`📊 WebP: ${summaryParts.join(', ')}`)

if (convertedCount > 0 && !options.dryRun) {
const totalSavings = totalOriginalSize - totalWebpSize
Expand All @@ -204,10 +208,8 @@ async function main() {
}

if (options.dryRun) {
console.log('\n💡 Run without --dry-run to perform actual conversion')
console.log('💡 Run without --dry-run to perform actual conversion')
}

console.log('')
}

main().catch((error) => {
Expand Down
21 changes: 0 additions & 21 deletions src/components/debug-tag.tsx

This file was deleted.

24 changes: 0 additions & 24 deletions src/components/deck.tsx

This file was deleted.

3 changes: 0 additions & 3 deletions src/components/game-error.tsx

This file was deleted.

7 changes: 0 additions & 7 deletions src/components/stats.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
gap: var(--spacing-100);
}

.stat {
display: flex;
align-items: center;
justify-content: center;
color: var(--color);
}

.stat-icon {
border-radius: var(--rounding-100);
min-width: var(--size-150);
Expand Down
20 changes: 0 additions & 20 deletions src/components/stats.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { clsx } from 'clsx'
import css from './stats.module.css'

export function Stats({
children,
className,
}: {
children: React.ReactNode
className?: string | undefined
}) {
return <div className={clsx(css['stats'], className)}>{children}</div>
}

export function StatsRow({
children,
className,
Expand All @@ -21,16 +11,6 @@ export function StatsRow({
return <div className={clsx(css['stats-row'], className)}>{children}</div>
}

export function Stat({
children,
className,
}: {
children: React.ReactNode
className?: string | undefined
}) {
return <div className={clsx(css['stat'], className)}>{children}</div>
}

export function StatIcon({ src, className }: { src: string; className?: string | undefined }) {
return <img src={src} className={clsx(css['stat-icon'], className)} />
}
Expand Down
5 changes: 0 additions & 5 deletions src/events/index.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/helpers/array.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/helpers/cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function defineCard(config: Omit<Card, 'id' | 'artwork' | 'sfx'>) {
}

/** Returns a card from a deck by its id */
export function getCard(id: string, deck: readonly Card[]): Card | undefined {
function getCard(id: string, deck: readonly Card[]): Card | undefined {
return deck.find((card) => card.id === id)
}

Expand Down
2 changes: 1 addition & 1 deletion src/helpers/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function defineItem(config: Omit<Item, 'id' | 'artwork' | 'sfx'>) {
}

/** Returns an item from an array by its id */
export function getItem(id: string, items: readonly Item[]): Item | undefined {
function getItem(id: string, items: readonly Item[]): Item | undefined {
return items.find((item) => item.id === id)
}

Expand Down
2 changes: 1 addition & 1 deletion src/helpers/rng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function shuffle<T>(xs: T[], randomIndex: (maxExclusive: number) => number): T[]
return arr
}

export class MathRandomRng implements Rng {
class MathRandomRng implements Rng {
int(maxExclusive: number): number {
return Math.floor(Math.random() * maxExclusive)
}
Expand Down
6 changes: 0 additions & 6 deletions src/helpers/string.ts

This file was deleted.

3 changes: 0 additions & 3 deletions src/types/env.ts

This file was deleted.

19 changes: 0 additions & 19 deletions src/types/player.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/types/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const SPACING = ['100', '200', '300', '400', '500'] as const
const SPACING = ['100', '200', '300', '400', '500'] as const

export type Spacing = (typeof SPACING)[number]
5 changes: 3 additions & 2 deletions vite-plugin-webp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ export function webpConversion(): Plugin {
if (hasRun) return
hasRun = true

console.log('\n🖼️ Converting images to WebP...')
console.log('🖼️ Converting images to WebP...')

try {
const { stdout } = await execAsync(
'node --experimental-strip-types scripts/convert-to-webp.ts',
)
console.log(stdout)
const output = stdout.trim()
if (output) console.log(output)
} catch (error) {
console.error('❌ WebP conversion failed:', error)
// Don't fail the build, just warn
Expand Down
15 changes: 1 addition & 14 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import { defineConfig, type PluginOption } from 'vite'
import react from '@vitejs/plugin-react'
import compression from 'vite-plugin-compression'
import imageMin from 'vite-plugin-imagemin'
import { imagetools } from 'vite-imagetools'
import { webpConversion } from './vite-plugin-webp'
import { webpConversion } from './vite-plugin-webp.ts'

const DEFAULT_PLUGINS: PluginOption = [
react(),
webpConversion(), // Run WebP conversion before other image processing
imagetools(),
imageMin({
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 20,
},
pngquant: {
quality: [0.8, 0.9],
speed: 4,
},
}),
]

// https://vitejs.dev/config/
Expand Down
Loading