npm->pnpm (#2929)
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2026-03-31 20:38:22 +03:00
committed by GitHub
parent 9b1960bbde
commit bd688d637c
27 changed files with 10782 additions and 15837 deletions

View File

@@ -18,7 +18,7 @@ description: Run, debug, and manage Playwright e2e tests. Use when running e2e t
Before running tests, check for these common issues:
1. **Stale worker databases** — Files matching `db-test-e2e-*.sqlite3` in the project root can cause "table already exists" migration errors if the schema has changed since they were created. Run `npm run test:e2e:generate-seeds` to regenerate these from the seed databases.
1. **Stale worker databases** — Files matching `db-test-e2e-*.sqlite3` in the project root can cause "table already exists" migration errors if the schema has changed since they were created. Run `pnpm run test:e2e:generate-seeds` to regenerate these from the seed databases.
2. **Port conflicts** — Check if anything is already listening on the e2e ports (base port through base+3):
```
@@ -26,7 +26,7 @@ Before running tests, check for these common issues:
```
If ports are occupied by leftover e2e servers, kill them. If occupied by something else, warn the user.
3. **Seed databases exist** — Verify `e2e/seeds/` contains the expected seed files. If missing, run `npm run test:e2e:generate-seeds`.
3. **Seed databases exist** — Verify `e2e/seeds/` contains the expected seed files. If missing, run `pnpm run test:e2e:generate-seeds`.
4. **Docker running** — MinIO requires Docker. Check with `docker info` if there are storage-related failures.
@@ -34,22 +34,22 @@ Before running tests, check for these common issues:
### Run all tests
```bash
npm run test:e2e
pnpm run test:e2e
```
### Run a specific test file
```bash
npx playwright test e2e/<name>.spec.ts
pnpm exec playwright test e2e/<name>.spec.ts
```
### Flaky detection (repeats each test 10 times, stops on first failure)
```bash
npm run test:e2e:flaky-detect
pnpm run test:e2e:flaky-detect
```
### Regenerate seed databases (after schema/migration changes)
```bash
npm run test:e2e:generate-seeds
pnpm run test:e2e:generate-seeds
```
## Debugging failures
@@ -65,19 +65,19 @@ Common infrastructure errors and fixes:
- **"table already exists"** → Stale worker DBs. Run `rm -f db-test-e2e-*.sqlite3`
- **"Server on port X did not start within timeout"** → Port conflict or app build error. Check ports with `lsof -i :<port>` and check for build errors
- **"MinIO failed to start"** → Docker not running or compose issue. Check `docker info`
- **Seed-related errors** → Run `npm run test:e2e:generate-seeds`
- **Seed-related errors** → Run `pnpm run test:e2e:generate-seeds`
### Step 3: Reduce to single debug worker
If the error is unclear, re-run with debug output and a single worker to see server logs:
```bash
E2E_DEBUG=true E2E_WORKERS=1 npx playwright test e2e/<failing-test>.spec.ts
E2E_DEBUG=true E2E_WORKERS=1 pnpm exec playwright test e2e/<failing-test>.spec.ts
```
This shows stdout/stderr from the test server, which is hidden by default.
### Step 4: Examine trace artifacts
Playwright is configured with `trace: "retain-on-failure"`. After a failure, view the trace:
```bash
npx playwright show-trace test-results/<test-folder>/trace.zip
pnpm exec playwright show-trace test-results/<test-folder>/trace.zip
```
## Test pattern reference

View File

@@ -4,6 +4,8 @@ updates:
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 8
groups:
minor-and-patch:
update-types:

View File

@@ -38,19 +38,21 @@ jobs:
echo "MinIO failed to start"
exit 1
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache: 'pnpm'
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: npx playwright install --with-deps
run: pnpm exec playwright install --with-deps
- name: Run E2E tests
run: npm run test:e2e
run: pnpm run test:e2e
- name: Stop MinIO
if: always()

View File

@@ -12,37 +12,33 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
cache: "pnpm"
- name: Install dependencies
run: npm ci
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: npx playwright install chromium
run: pnpm exec playwright install chromium
- name: Formatter/Linter
run: npm run biome:check
run: pnpm run biome:check
- name: Typecheck
run: npm run typecheck
run: pnpm run typecheck
- name: Unit tests
run: npm run test:unit:browser
run: pnpm run test:unit:browser
- name: Knip unused check
run: npm run knip
run: pnpm run knip
- name: Check translations jsons
run: npm run check-translation-jsons:no-write
run: pnpm run check-translation-jsons:no-write
- name: Check homemade badges
run: npm run check-homemade-badges
run: pnpm run check-homemade-badges
- name: Check articles
run: npm run check-articles
run: pnpm run check-articles
- name: Check test DB migrations
run: npm run check-test-db-migrations
run: pnpm run check-test-db-migrations

View File

@@ -11,26 +11,22 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
restore-keys: npm-
cache: "pnpm"
- name: Install dependencies
run: npm ci --ignore-scripts
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Update translation progress issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm run check-translation-jsons
pnpm run check-translation-jsons
gh issue edit 1104 --body-file ./translation-progress.md

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules
package-lock.json
/.cache*
/build

View File

@@ -2,18 +2,18 @@
- only rarely use comments, prefer descriptive variable and function names (leave existing comments as is)
- if you encounter an existing TODO comment assume it is there for a reason and do not remove it
- task is not considered completely until `npm run checks` passes
- task is not considered completely until `pnpm run checks` passes
- normal file structure has constants at the top immediately followed by the main function body of the file. Helpers are used to structure the code and they are at the bottom of the file (main implementation first, at the top of the file)
- note: any formatting issue (such as tabs vs. spaces) can be resolved by running the `npm run biome:fix` command
- note: any formatting issue (such as tabs vs. spaces) can be resolved by running the `pnpm run biome:fix` command
## Commands
- `npm run typecheck` runs TypeScript type checking
- `npm run biome:fix` runs Biome code formatter and linter
- `npm run test:unit:browser` runs all unit tests and browser tests
- `npm run test:e2e` runs all e2e tests
- `npm run test:e2e:flaky-detect` runs all e2e tests and repeats each 10 times
- `npm run i18n:sync` syncs translation jsons with English
- `pnpm run typecheck` runs TypeScript type checking
- `pnpm run biome:fix` runs Biome code formatter and linter
- `pnpm run test:unit:browser` runs all unit tests and browser tests
- `pnpm run test:e2e` runs all e2e tests
- `pnpm run test:e2e:flaky-detect` runs all e2e tests and repeats each 10 times
- `pnpm run i18n:sync` syncs translation jsons with English
## Typescript
@@ -75,7 +75,7 @@
- by default everything should be translated via i18next
- some a11y labels or text that should not normally be encountered by user (example given, error message by server) can be english
- before adding a new translation, check that one doesn't already exist you can reuse (particularly in the common.json)
- add only English translation and use `npm run i18n:sync` to initialize other jsons with empty string ready for translators
- add only English translation and use `pnpm run i18n:sync` to initialize other jsons with empty string ready for translators
## Commit messages

View File

@@ -82,8 +82,8 @@ Then there is a sequence of commands you need to run:
```bash
git clone https://github.com/sendou-ink/sendou.ink.git # Clones repository
cd sendou.ink # Change to the project's folder
npm install # Install dependencies
npm run dev # Setup the development environment and run the project
pnpm install # Install dependencies
pnpm dev # Setup the development environment and run the project
```
You should then be able to access the application by visiting http://localhost:5173

View File

@@ -1,329 +0,0 @@
# Tournament LFG Implementation Plan
## Overview
Add a new `/to/:id/looking` route that provides SendouQ-style matchmaking for tournament team formation. Players and teams can find each other before the tournament starts.
## Phase 1: Database Migration
**File**: `migrations/118-tournament-lfg.js`
Create three new tables:
### TournamentLFGGroup
| Column | Type | Notes |
|--------|------|-------|
| id | integer primary key | Auto-increment |
| tournamentId | integer not null | FK to Tournament |
| tournamentTeamId | integer | FK to TournamentTeam (null for unregistered groups) |
| visibility | text | JSON (AssociationVisibility) |
| chatCode | text not null | Unique room code for group chat |
| createdAt | integer | Default now |
### TournamentLFGGroupMember
| Column | Type | Notes |
|--------|------|-------|
| groupId | integer not null | FK to TournamentLFGGroup |
| userId | integer not null | FK to User |
| role | text not null | OWNER / MANAGER / REGULAR |
| note | text | Public note |
| isStayAsSub | integer | Boolean (0/1), default 0 |
| createdAt | integer | Default now |
### TournamentLFGLike
| Column | Type | Notes |
|--------|------|-------|
| likerGroupId | integer not null | FK to TournamentLFGGroup |
| targetGroupId | integer not null | FK to TournamentLFGGroup |
| createdAt | integer | Default now |
All tables use `STRICT` mode, `ON DELETE CASCADE` for FKs, and have indexes on FK columns.
---
## Phase 2: TypeScript Types
**File**: `app/db/tables.ts`
Add interfaces for the three new tables following existing patterns (`GeneratedAlways`, `Generated`, `JSONColumnTypeNullable`).
Add to the `DB` interface:
- `TournamentLFGGroup`
- `TournamentLFGGroupMember`
- `TournamentLFGLike`
---
## Phase 3: Feature File Structure
```
app/features/tournament-lfg/
├── TournamentLFGRepository.server.ts # Database operations
├── tournament-lfg-types.ts # TypeScript types (LFGGroup, LFGGroupMember, etc.)
├── tournament-lfg-schemas.server.ts # Zod validation schemas
├── tournament-lfg-constants.ts # Constants (note max length, etc.)
├── tournament-lfg-utils.ts # Utility functions
├── routes/
│ └── to.$id.looking.tsx # Main route (loader + action + component)
├── loaders/
│ └── to.$id.looking.server.ts # Data loader
├── actions/
│ └── to.$id.looking.server.ts # Action handler
└── components/
└── LFGGroupCard.tsx # Group card (mirrors SendouQ GroupCard pattern)
```
---
## Phase 4: Repository Implementation
**File**: `app/features/tournament-lfg/TournamentLFGRepository.server.ts`
Mirror `SQGroupRepository.server.ts` pattern. Key functions:
**Group Management:**
- `findGroupsByTournamentId(tournamentId)` - Get all active groups
- `addMember(groupId, { userId, role, stayAsSub? })` - Add member to group
- `morphGroups({ survivingGroupId, otherGroupId })` - Merge two groups
**Likes:**
- `addLike({ likerGroupId, targetGroupId })` - Add like
- `deleteLike({ likerGroupId, targetGroupId })` - Remove like
- `allLikesByGroupId(groupId)` - Get { given: [], received: [] }
**Member Management:**
- `updateMemberNote({ groupId, userId, value })` - Update public note
- `updateMemberRole({ userId, groupId, role })` - Change role
- `updateStayAsSub({ groupId, userId, value })` - Toggle sub preference
- `kickMember({ groupId, userId })` - Owner kicks member
**Tournament Integration:**
- `cleanupForTournamentStart(tournamentId)` - Delete groups, preserve stayAsSub members
- `getSubsForTournament(tournamentId)` - Get users who opted to stay as sub
---
## Phase 5: Route Implementation
### 5.1 Route Registration
**File**: `app/routes.ts`
Add inside `/to/:id` children:
```typescript
route("looking", "features/tournament-lfg/routes/to.$id.looking.tsx"),
```
### 5.2 Loader
**File**: `app/features/tournament-lfg/loaders/to.$id.looking.server.ts`
Returns:
- `groups` - All visible groups (filtered by visibility)
- `ownGroup` - User's current group (if any)
- `likes` - { given: [], received: [] } for own group
- `privateNotes` - User's private notes on other players (reuse from SendouQ)
- `lastUpdated` - Timestamp for auto-refresh
### 5.3 Action
**File**: `app/features/tournament-lfg/actions/to.$id.looking.server.ts`
Actions:
- `JOIN_QUEUE` - Create new group
- `LIKE` / `UNLIKE` - Like/unlike another group
- `ACCEPT` - Accept mutual like (triggers team creation/merge)
- `LEAVE_GROUP` - Leave current group
- `KICK_FROM_GROUP` - Owner kicks member
- `GIVE_MANAGER` / `REMOVE_MANAGER` - Role management
- `UPDATE_NOTE` - Update public note
- `UPDATE_STAY_AS_SUB` - Toggle sub preference
- `REFRESH_GROUP` - Refresh activity timestamp
**ACCEPT Action Flow:**
1. Verify mutual like exists
2. Check if either group has `tournamentTeamId`
3. If neither: Create new `TournamentTeam` (use auto-generated name)
4. Merge groups (use `morphGroups`)
5. Add all members to `TournamentTeamMember`
6. Send `TO_LFG_TEAM_FORMED` notification
7. If team reaches `maxMembersPerTeam`: delete the LFG group
### 5.4 Component
**File**: `app/features/tournament-lfg/routes/to.$id.looking.tsx`
Structure (mirror `/q/looking`):
- Three-column desktop layout: My Group | Groups | Invitations
- Tab structure on mobile
- Reuse `GroupCard` display pattern (weapons, VC, tier)
- Reuse `MemberAdder` for quick-add trusted players
- Reuse `GroupLeaver` component
- Chat integration for groups with 2+ members
- "Stay as sub" checkbox in join form
---
## Phase 6: Tournament Integration
### 6.1 Add "Looking" Tab
**File**: `app/features/tournament/routes/to.$id.tsx`
Add new `SubNavLink` (show only before tournament starts, not for invitationals):
```tsx
{!tournament.hasStarted && !tournament.isInvitational && (
<SubNavLink to="looking">{t("tournament:tabs.looking")}</SubNavLink>
)}
```
### 6.2 Tournament.ts Getter
**File**: `app/features/tournament-bracket/core/Tournament.ts`
Add:
```typescript
get lfgEnabled() {
return !this.isInvitational && !this.hasStarted && this.subsFeatureEnabled;
}
```
### 6.3 Auto-cleanup on Tournament Start
When tournament bracket starts, call `TournamentLFGRepository.cleanupForTournamentStart(tournamentId)`:
- Delete all `TournamentLFGGroup` records
- Preserve `TournamentLFGGroupMember` records where `stayAsSub = 1` for subs list
---
## Phase 7: Notifications
**File**: `app/features/notifications/notifications-types.ts`
Add three new notification types:
```typescript
| NotificationItem<
"TO_LFG_LIKED",
{
tournamentId: number;
tournamentName: string;
likerUsername: string;
}
>
| NotificationItem<
"TO_LFG_TEAM_FORMED",
{
tournamentId: number;
tournamentName: string;
teamName: string;
tournamentTeamId: number;
}
>
| NotificationItem<
"TO_LFG_CHAT_MESSAGE",
{
tournamentId: number;
tournamentName: string;
teamName: string;
tournamentTeamId: number;
}
>
```
**File**: `app/features/notifications/notifications-utils.ts`
Add notification link handlers and icon mappings.
---
## Phase 8: Translations
**File**: `public/locales/en/tournament.json`
Add keys:
- `tabs.looking`
- `lfg.join.header`, `lfg.join.stayAsSub`, `lfg.join.visibility`
- `lfg.myGroup.header`, `lfg.myGroup.empty`
- `lfg.groups.header`, `lfg.groups.empty`
- `lfg.invitations.header`, `lfg.invitations.empty`, `lfg.invitations.accept`
- `lfg.actions.like`, `lfg.actions.unlike`, `lfg.actions.leave`
Run `npm run i18n:sync` after adding.
---
## Phase 9: Group Merging Logic
When two groups merge via ACCEPT:
| Group A has team | Group B has team | Result |
|------------------|------------------|--------|
| No | No | Create new TournamentTeam, both join it |
| Yes | No | B's members join A's team |
| No | Yes | A's members join B's team |
| Yes | Yes | Accepting team absorbs liker team (accepting team name persists) |
Auto-generated team name format: `"<owner_username>'s Team"` (e.g., "Sendou's Team" - can be changed later on registration page)
After merge:
- Combined group stays in LFG queue
- When `maxMembersPerTeam` reached, group is auto-removed from queue
---
## Key Files to Reference
| Purpose | File |
|---------|------|
| Repository pattern | `app/features/sendouq/SQGroupRepository.server.ts` |
| Route pattern | `app/features/sendouq/routes/q.looking.tsx` |
| Action pattern | `app/features/sendouq/actions/q.looking.server.ts` |
| GroupCard UI | `app/features/sendouq/components/GroupCard.tsx` |
| Tournament tabs | `app/features/tournament/routes/to.$id.tsx` |
| Team creation | `app/features/tournament/TournamentTeamRepository.server.ts` |
| Notification types | `app/features/notifications/notifications-types.ts` |
| Visibility type | `app/features/associations/associations-types.ts` |
---
## Implementation Order
1. Migration (100-tournament-lfg.js)
2. Types (tables.ts + tournament-lfg-types.ts)
3. Repository (TournamentLFGRepository.server.ts)
4. Schemas (tournament-lfg-schemas.server.ts)
5. Loader (to.$id.looking.server.ts)
6. Action (to.$id.looking.server.ts)
7. Route component (to.$id.looking.tsx)
8. Tournament integration (tab, cleanup hook)
9. Notifications (types + utils)
10. Translations
11. Testing
---
## Verification
1. **Manual Testing:**
- Join LFG as solo player
- Create group with 2 players
- Like another group, verify notification sent
- Accept mutual like, verify team created
- Verify team appears on registration page
- Test "stay as sub" checkbox
- Test visibility filtering
2. **Unit Tests:**
- Repository functions (create, merge, delete, likes)
- Visibility filtering logic
3. **E2E Tests:**
- Full flow: join -> like -> accept -> team formed
- Leave group
- Kick from group
4. **Run checks:**
```bash
npm run checks
```

View File

@@ -1,166 +0,0 @@
# Tournament LFG Feature Spec
## Overview
New `/to/:id/looking` route that provides SendouQ-style matchmaking for tournament team formation. Players and teams can find each other before tournament starts.
## Route
`/to/:id/looking` (new route, mirrors `/q/looking`)
## Data Model
Separate tables from SendouQ (cleaner separation):
### TournamentLFGGroup
| Column | Type | Description |
|--------|------|-------------|
| id | number | Primary key |
| tournamentTeamId | number | null | FK to TournamentTeam |
| visibility | string | JSON visibility info (/scrims style) |
| chatCode | string | Unique room code for group chat |
| createdAt | number | Timestamp |
### TournamentLFGGroupMember
| Column | Type | Description |
|--------|------|-------------|
| groupId | number | FK to TournamentLFGGroup |
| userId | number | FK to User |
| role | string | OWNER / MANAGER / REGULAR |
| note | string | Public note visible to group members |
| stayAsSub | boolean | Convert to sub if team not formed by start |
| createdAt | number | Timestamp |
### TournamentLFGLike
| Column | Type | Description |
|--------|------|-------------|
| likerGroupId | number | FK to TournamentLFGGroup |
| targetGroupId | number | FK to TournamentLFGGroup |
| createdAt | number | Timestamp |
### TournamentSub
Redundant, removed.
## Who Can Join
- **Solo players** - Looking for a team
- **Partial groups (2-3 players)** - Looking for more members
- **Already-registered tournament teams** - Recruiting up to `maxMembersPerTeam`
## Features
### Joining the Queue
- Players reuse weapon/VC data from `/q/settings` (`User.qWeaponPool`, `User.vc`, `User.languages`)
- Checkbox on join: "Add me as sub if I don't find a team"
- Support for notes (like SendouQ public member notes)
- Uses schema based SendouForm (forms.md for details)
### Visibility System (Scrims-style)
- **Base visibility**: team/org/public
- **Not-found visibility**: Time-delayed expansion if no match found
- Uses existing `AssociationVisibility` system from scrims
### Likes & Matching
1. Players/groups can like each other
2. Target group receives `TO_LFG_LIKED` notification
3. On mutual like, accepting party sees accept button
4. Accept click triggers:
- If neither party is registered team → create a team (some default name is used)
- If one party is registered team → Other party joins that team
- If both parties are teams, the accepting team absorbs the liker team (accepting team's name is used for the new merged team)
5. After merge, combined group stays in queue to recruit more members
### Team Formation
- **Immediate registration**: When first two players merge, default name is used
- Newly formed team stays in LFG queue
- Teams can grow up to `maxMembersPerTeam` (typically 6 for 4v4)
- When `maxMembersPerTeam` is reached, team is automatically removed from the queue
- Solo players liking registered teams get absorbed as new members
### Tournament Start Auto-Cleanup
When tournament starts:
1. All unregistered LFG groups are deleted
2. Players who checked "stay as sub" are shown in a simple list "`TournamentLFGGroupMember` reused here even if they technically are no longer members of anything)
3. Their sub data uses existing `/q/settings` weapon/VC preferences
## Notifications
| Type | When | Meta |
|------|------|------|
| `TO_LFG_LIKED` | Someone likes your group | `{ tournamentId, tournamentName, likerUsername }` |
| `TO_LFG_TEAM_FORMED` | You join/form a team via LFG | `{ tournamentId, tournamentName, teamName, tournamentTeamId }` |
| `TO_LFG_CHAT_MESSAGE` | Chat message sent | `{ tournamentId, tournamentName, teamName, tournamentTeamId }` |
## UI
### Reuse from `/q/looking`
- `GroupCard` component (weapons, VC, tier display)
- Tab structure (My Group, Groups, Invitations)
- `MemberAdder` component (invite link, quick add), note for these same invite link and quick add endpoint is used as on /to/:id/register page
- `GroupLeaver` component
- Private user notes system
### Tab Structure
1. **My Group** - Current group members, invite link, leave button, chat (if 2+ members)
2. **Groups** - Other groups looking, with like/unlike buttons
3. **Invitations** - Groups that have liked your group, with accept/decline
### Accept Flow
Simple button click (SendouQ-style)
## Files to Create/Modify
### New Files
```
app/features/tournament-lfg/
├── core/
│ └── TournamentLFG.server.ts # Main class (like SendouQ.server.ts)
├── routes/
│ ├── to.$id.looking.tsx # Main LFG page
│ └── to.$id.looking.new.tsx # Join LFG form (if needed)
├── loaders/
│ └── to.$id.looking.server.ts # Data loader
├── actions/
│ └── to.$id.looking.server.ts # Action handler
├── components/
│ └── (reuse from sendouq where possible)
├── TournamentLFGRepository.server.ts # Database queries
├── tournament-lfg-types.ts # TypeScript types
├── tournament-lfg-schemas.server.ts # Zod validation
└── tournament-lfg-constants.ts # Constants
```
### Migrations
```
migrations/XXX-tournament-lfg.js # Create new tables
```
### Modified Files
- `app/routes.ts` - Add new route
- `app/features/notifications/notifications-types.ts` - Add new notification types
- `app/features/tournament-bracket/core/Tournament.ts` - Add LFG-related getters
- `app/features/tournament/components/TournamentTabs.tsx` - Add "Looking" tab
## Differences to SendouQ
- no invite code (tournament teams have their own invite code by default)
- no expiredAt
## Open Questions
- How we should define the autogenerated tournament team name?

View File

@@ -2,7 +2,7 @@
/* https://github.com/tldraw/tldraw/blob/2352985e949d14270fc89dc60144239c37c8ff91/packages/tldraw/src/hooks/useStylesheet.ts */
@import url("https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block");
@import url("tldraw/tldraw.css");
@import url("@tldraw/tldraw/tldraw.css");
@font-face {
font-family: "Recursive";

View File

@@ -15,7 +15,7 @@ import { IS_E2E_TEST_RUN } from "~/utils/e2e";
* console.log(Seasons.list[0].starts); // Logs the start date of the first season
*/
export const list =
// when we do npm run setup NODE_ENV is not set -> use test seasons
// when we do pnpm run setup NODE_ENV is not set -> use test seasons
!process.env.NODE_ENV ||
IS_E2E_TEST_RUN ||
// this gets checked when the project is running

View File

@@ -1,5 +1,5 @@
// Auto-generated team name data from adj.json and sub.json
// Regenerate with: npx tsx scripts/generate-team-names.ts
// Regenerate with: pnpm exec tsx scripts/generate-team-names.ts
export const ADJECTIVES = [
"Inkless",

View File

@@ -567,7 +567,7 @@ NOTE: before adding a new one, verify one does not already exist.
- Select options: `options.fieldName.value`
- Mode names: `modes.SZ`, `modes.TC`, etc.
Run `npm run i18n:sync` after adding English translations to initialize other language files.
Run `pnpm run i18n:sync` after adding English translations to initialize other language files.
## E2E Testing

View File

@@ -4,7 +4,7 @@ Guides on how to do different things when developing sendou.ink
## Fix style/lint errors (Biome)
Run the `npm run biome:fix` command. Also you might want to set up Biome as an extension to your IDE and run automatically when you save a file.
Run the `pnpm run biome:fix` command. Also you might want to set up Biome as an extension to your IDE and run automatically when you save a file.
## Add a new database migration
@@ -22,8 +22,8 @@ export function up(db) {
Note: No need to implement the "down" migration
3) Update the typings in `app/db/tables.ts`
4) Run `npm run migrate up` to apply your migration
4) Set env var `DB_PATH=db-test.sqlite3` in `.env` file & run the `npm run migrate up` command again to update the database used in unit tests
4) Run `pnpm run migrate up` to apply your migration
4) Set env var `DB_PATH=db-test.sqlite3` in `.env` file & run the `pnpm run migrate up` command again to update the database used in unit tests
## Add a new translation string
@@ -55,4 +55,4 @@ When utilizing feature specific translations ensure the json is loaded. This is
### Sync
Use the `npm run i18n:sync` command to sync translation jsons with English (removing and adding keys for each language as needed). There is not currently a check in the pipeline that this was done but it should always be ran when a new translation string has been added or removed.
Use the `pnpm run i18n:sync` command to sync translation jsons with English (removing and adding keys for each language as needed). There is not currently a check in the pipeline that this was done but it should always be ran when a new translation string has been added or removed.

View File

@@ -7,19 +7,19 @@ Note: These are mostly useful if you are running the site in production as an ad
## Add new badge to the database
```bash
npx tsx scripts/add-badge.ts fire_green "Octofin Eliteboard"
pnpm exec tsx scripts/add-badge.ts fire_green "Octofin Eliteboard"
```
## Rename display name of a badge
```bash
npx tsx scripts/rename-badge.ts 10 "New 4v4 Sundaes"
pnpm exec tsx scripts/rename-badge.ts 10 "New 4v4 Sundaes"
```
## Add many badge owners
```bash
npx tsx scripts/add-badge-winners.ts 10 "750705955909664791,79237403620945920"
pnpm exec tsx scripts/add-badge-winners.ts 10 "750705955909664791,79237403620945920"
```
## Converting gifs (badges) to thumbnail (.png)
@@ -33,7 +33,7 @@ sips -s format png ./sundae.gif --out .
While in the folder with the images:
```bash
for i in *.png; do npx @squoosh/cli --avif '{"cqLevel":33,"cqAlphaLevel":-1,"denoiseLevel":0,"tileColsLog2":0,"tileRowsLog2":0,"speed":6,"subsample":1,"chromaDeltaQ":false,"sharpness":0,"tune":0}' $i; done
for i in *.png; do pnpm dlx @squoosh/cli --avif '{"cqLevel":33,"cqAlphaLevel":-1,"denoiseLevel":0,"tileColsLog2":0,"tileRowsLog2":0,"speed":6,"subsample":1,"chromaDeltaQ":false,"sharpness":0,"tune":0}' $i; done
```
Note: it only works with Node 16.
@@ -48,19 +48,19 @@ Note: it only works with Node 16.
1. Update `CURRENT_PATCH` constants
1. Update `PATCHES` constant with the late patch + remove the oldest
1. Update the stage list in `stage-ids.ts` and `create-misc-json.ts`. Add images from Lean's repository and avify them.
1. `npx tsx scripts/create-misc-json.ts`
1. `npx tsx scripts/create-gear-json.ts`
1. `npx tsx scripts/create-analyzer-json.ts`
1. `pnpm exec tsx scripts/create-misc-json.ts`
1. `pnpm exec tsx scripts/create-gear-json.ts`
1. `pnpm exec tsx scripts/create-analyzer-json.ts`
8a. Double check that no hard-coded special damages changed
1. `npx tsx scripts/create-object-dmg-json.ts`
1. `pnpm exec tsx scripts/create-object-dmg-json.ts`
1. Fill new weapon IDs by category to `weapon-ids.ts` (easy to take from the diff of English weapons.json)
1. Get gear IDs for each slot from /output folder and update `gear-ids.ts`.
1. Replace `object-dmg.json` with the `object-dmg.json` in /output folder
1. Replace `weapon-params.ts` with the `params.json` in /output folder
1. Delete all images inside `main-weapons`, `main-weapons-outlined`, `main-weapons-outlined-2` and `gear` folders.
1. Replace with images from Lean's repository.
1. Run the `npx tsx scripts/replace-img-names.ts` command
1. Run the `npx tsx scripts/replace-weapon-names.ts` command
1. Run the `pnpm exec tsx scripts/replace-img-names.ts` command
1. Run the `pnpm exec tsx scripts/replace-weapon-names.ts` command
1. Run the .avif generating command in each image folder.
2. Update manually any languages that use English `gear.json` and `weapons.json` files

View File

@@ -7,7 +7,7 @@ This guide explains how to profile and optimize slow routes in sendou.ink.
Install autocannon globally:
```bash
npm install -g autocannon
pnpm add -g autocannon
```
## Step 1: Identify Slow Routes
@@ -100,7 +100,7 @@ db.close();
Run with:
```bash
npx tsx profile-query.ts
pnpm exec tsx profile-query.ts
```
## Step 3: Analyze Query Plans
@@ -221,7 +221,7 @@ After applying fixes, restart the dev server and re-run autocannon:
```bash
# Restart server to pick up changes
# (Ctrl+C and npm run dev)
# (Ctrl+C and pnpm dev)
# Re-benchmark
autocannon -c 10 -d 10 http://localhost:301/leaderboards
@@ -246,7 +246,7 @@ console.log(`Rows: ${result.length}`);
db.close();
EOF
npx tsx profile.ts
pnpm exec tsx profile.ts
# 3. Clean up
rm profile.ts

View File

@@ -96,7 +96,7 @@ async function globalSetup(_config: FullConfig) {
// Use port 6173 as the base - tests will rewrite URLs as needed
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log("Building the application...");
execSync("npm run build", {
execSync("pnpm run build", {
stdio: "inherit",
env: {
...process.env,
@@ -125,13 +125,13 @@ async function globalSetup(_config: FullConfig) {
if (!fs.existsSync(dbPath)) {
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Setting up database for worker ${i}: ${dbPath}`);
execSync(`DB_PATH=${dbPath} npm run migrate up`, { stdio: "inherit" });
execSync(`DB_PATH=${dbPath} pnpm run migrate up`, { stdio: "inherit" });
}
// Start server
// biome-ignore lint/suspicious/noConsole: CLI script output
console.log(`Starting server for worker ${i} on port ${port}...`);
const serverProcess = spawn("npm", ["start"], {
const serverProcess = spawn("pnpm", ["start"], {
env: {
...process.env,
DB_PATH: dbPath,

15164
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,125 +2,132 @@
"name": "sendou.ink",
"version": "3.0.0",
"private": true,
"packageManager": "pnpm@10.33.0",
"sideEffects": false,
"type": "module",
"scripts": {
"deploy": "npm ci && npm run build",
"deploy": "pnpm install --frozen-lockfile && pnpm run build",
"build": "react-router build",
"dev": "cross-env DB_PATH=db.sqlite3 npm run migrate up && npm run setup && react-router dev --host",
"dev": "cross-env DB_PATH=db.sqlite3 pnpm run migrate up && pnpm run setup && react-router dev --host",
"dev:prod": "cross-env DB_PATH=db-prod.sqlite3 VITE_PROD_MODE=true react-router dev --host",
"start": "npm run migrate up && react-router-serve build/server/index.js",
"start": "pnpm run migrate up && react-router-serve build/server/index.js",
"migrate": "ley",
"migrate:prod": "cross-env DB_PATH=db-prod.sqlite3 npm run migrate up",
"migrate:prod": "cross-env DB_PATH=db-prod.sqlite3 pnpm run migrate up",
"check-translation-jsons": "node --experimental-strip-types scripts/check-translation-jsons.ts",
"check-translation-jsons:no-write": "node --experimental-strip-types scripts/check-translation-jsons.ts --no-write",
"check-homemade-badges": "node --experimental-strip-types scripts/check-homemade-badges.ts",
"check-articles": "tsx scripts/check-articles.ts",
"refresh-prod-db": "node --experimental-strip-types scripts/refresh-prod-db.ts && npm run migrate:prod",
"biome:check": "npx @biomejs/biome check --error-on-warnings .",
"biome:fix": "npx @biomejs/biome check --error-on-warnings --write .",
"biome:fix:unsafe": "npx @biomejs/biome check --error-on-warnings --write --unsafe .",
"refresh-prod-db": "node --experimental-strip-types scripts/refresh-prod-db.ts && pnpm run migrate:prod",
"biome:check": "biome check --error-on-warnings .",
"biome:fix": "biome check --error-on-warnings --write .",
"biome:fix:unsafe": "biome check --error-on-warnings --write --unsafe .",
"typecheck": "react-router typegen && tsc --noEmit",
"test:unit:browser": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 BROWSER_HEADLESS=true vitest --silent=passed-only run",
"test:browser:ui": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only --project browser",
"test:unit:browser:ui": "cross-env VITE_SITE_DOMAIN=http://localhost:5173 vitest --silent=passed-only",
"test:e2e": "npx playwright test",
"test:e2e:flaky-detect": "npx playwright test --repeat-each=10 --max-failures=1",
"test:e2e:generate-seeds": "cross-env DB_PATH=db-test.sqlite3 npm run migrate up && cross-env DB_PATH=db-test.sqlite3 npx vite-node scripts/generate-e2e-seed-dbs.ts",
"test:e2e": "playwright test",
"test:e2e:flaky-detect": "playwright test --repeat-each=10 --max-failures=1",
"test:e2e:generate-seeds": "cross-env DB_PATH=db-test.sqlite3 pnpm run migrate up && cross-env DB_PATH=db-test.sqlite3 vite-node scripts/generate-e2e-seed-dbs.ts",
"check-test-db-migrations": "node --experimental-strip-types scripts/check-test-db-migrations.ts",
"checks": "npm run biome:fix && npm run test:unit:browser && npm run check-translation-jsons && npm run typecheck && npm run knip && npm run check-test-db-migrations",
"checks": "pnpm run biome:fix && pnpm run test:unit:browser && pnpm run check-translation-jsons && pnpm run typecheck && pnpm run knip && pnpm run check-test-db-migrations",
"setup": "cross-env DB_PATH=db.sqlite3 vite-node ./scripts/setup.ts",
"i18n:sync": "i18next-locales-sync -e true -p en -s da de es-ES es-US fr-CA fr-EU he it ja ko nl pl pt-BR ru zh -l locales && npm run biome:fix",
"i18n:sync": "i18next-locales-sync -e true -p en -s da de es-ES es-US fr-CA fr-EU he it ja ko nl pl pt-BR ru zh -l locales && pnpm run biome:fix",
"knip": "knip",
"sync-weapon-params": "tsx scripts/sync-weapon-params.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1019.0",
"@aws-sdk/lib-storage": "^3.1019.0",
"@date-fns/tz": "^1.4.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@epic-web/cachified": "^5.6.2",
"@faker-js/faker": "^10.4.0",
"@formatjs/intl-durationformat": "^0.10.3",
"@internationalized/date": "^3.12.0",
"@react-router/node": "^7.13.2",
"@react-router/serve": "^7.13.2",
"@remix-run/form-data-parser": "^0.16.0",
"@tldraw/tldraw": "^3.12.1",
"@zumer/snapdom": "^2.7.0",
"aws-sdk": "^2.1693.0",
"better-sqlite3": "^12.8.0",
"clsx": "^2.1.1",
"compressorjs": "^1.2.1",
"date-fns": "^4.1.0",
"edmonds-blossom-fixed": "^1.0.1",
"gray-matter": "^4.0.3",
"i18next": "^25.10.10",
"i18next-browser-languagedetector": "^8.2.1",
"i18next-http-backend": "^3.0.2",
"ics": "^3.11.0",
"isbot": "^5.1.36",
"jsoncrush": "^1.1.8",
"kysely": "^0.28.14",
"lru-cache": "^11.2.7",
"lucide-react": "^1.7.0",
"markdown-to-jsx": "^9.7.13",
"nanoid": "^5.1.7",
"neverthrow": "^8.2.0",
"@aws-sdk/client-s3": "3.1019.0",
"@aws-sdk/lib-storage": "3.1019.0",
"@date-fns/tz": "1.4.1",
"@dnd-kit/core": "6.3.1",
"@dnd-kit/modifiers": "9.0.0",
"@dnd-kit/sortable": "10.0.0",
"@dnd-kit/utilities": "3.2.2",
"@epic-web/cachified": "5.6.2",
"@faker-js/faker": "10.4.0",
"@formatjs/intl-durationformat": "0.10.3",
"@internationalized/date": "3.12.0",
"@react-router/node": "7.13.2",
"@react-router/serve": "7.13.2",
"@remix-run/form-data-parser": "0.16.0",
"@tldraw/tldraw": "3.12.1",
"@zumer/snapdom": "2.7.0",
"aws-sdk": "2.1693.0",
"better-sqlite3": "12.8.0",
"clsx": "2.1.1",
"compressorjs": "1.2.1",
"date-fns": "4.1.0",
"edmonds-blossom-fixed": "1.0.1",
"gray-matter": "4.0.3",
"i18next": "25.10.10",
"i18next-browser-languagedetector": "8.2.1",
"i18next-http-backend": "3.0.2",
"ics": "3.11.0",
"isbot": "5.1.36",
"jsoncrush": "1.1.8",
"kysely": "0.28.14",
"lru-cache": "11.2.7",
"lucide-react": "1.7.0",
"markdown-to-jsx": "9.7.13",
"nanoid": "5.1.7",
"neverthrow": "8.2.0",
"node-cron": "4.2.1",
"nprogress": "^0.2.0",
"openskill": "^4.1.1",
"p-limit": "^7.3.0",
"partysocket": "^1.1.16",
"react": "^19.2.4",
"react-aria-components": "^1.16.0",
"react-charts": "^3.0.0-beta.57",
"react-dom": "^19.2.4",
"react-error-boundary": "^6.1.1",
"nprogress": "0.2.0",
"openskill": "4.1.1",
"p-limit": "7.3.0",
"partysocket": "1.1.16",
"react": "19.2.4",
"react-aria-components": "1.16.0",
"react-charts": "3.0.0-beta.57",
"react-dom": "19.2.4",
"react-error-boundary": "6.1.1",
"react-flip-toolkit": "7.2.4",
"react-i18next": "^16.5.8",
"react-router": "^7.13.2",
"react-use": "^17.6.0",
"react-use-draggable-scroll": "^0.4.7",
"remeda": "^2.33.6",
"remix-auth": "^4.2.0",
"remix-auth-oauth2": "^3.4.1",
"remix-i18next": "^7.4.2",
"slugify": "^1.6.8",
"swr": "^2.4.1",
"web-push": "^3.6.7",
"zod": "^4.3.6"
"react-i18next": "16.5.8",
"react-router": "7.13.2",
"react-use": "17.6.0",
"react-use-draggable-scroll": "0.4.7",
"remeda": "2.33.6",
"remix-auth": "4.2.0",
"remix-auth-oauth2": "3.4.1",
"remix-i18next": "7.4.2",
"slugify": "1.6.8",
"swr": "2.4.1",
"web-push": "3.6.7",
"zod": "4.3.6"
},
"devDependencies": {
"@biomejs/biome": "2.4.9",
"@playwright/test": "^1.58.2",
"@react-router/dev": "^7.13.2",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.5.0",
"@types/node-cron": "^3.0.11",
"@types/nprogress": "^0.2.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/web-push": "^3.6.4",
"@vitest/browser-playwright": "^4.1.2",
"@vitest/ui": "^4.1.2",
"babel-plugin-react-compiler": "^19.1.0-rc.2",
"cross-env": "^10.1.0",
"dotenv": "^17.3.1",
"i18next-locales-sync": "^2.1.1",
"knip": "^6.0.6",
"ley": "^0.8.1",
"sql-formatter": "^15.7.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-node": "^5.3.0",
"vite-plugin-babel": "^1.6.0",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.2",
"vitest-browser-react": "^2.1.0"
"@playwright/test": "1.58.2",
"@react-router/dev": "7.13.2",
"@types/better-sqlite3": "7.6.13",
"@types/node": "25.5.0",
"@types/node-cron": "3.0.11",
"@types/nprogress": "0.2.3",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/web-push": "3.6.4",
"@vitest/browser-playwright": "4.1.2",
"@vitest/ui": "4.1.2",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cross-env": "10.1.0",
"dotenv": "17.3.1",
"i18next-locales-sync": "2.1.1",
"knip": "6.0.6",
"ley": "0.8.1",
"sql-formatter": "15.7.2",
"tsx": "4.21.0",
"typescript": "5.9.3",
"vite": "7.3.1",
"vite-node": "5.3.0",
"vite-plugin-babel": "1.6.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.2",
"vitest-browser-react": "2.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3",
"esbuild"
]
}
}

10600
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
/**
* Backfill Tournament Tiers Script
*
* Run with: npx tsx scripts/backfill-tournament-tiers.ts
* Run with: pnpm exec tsx scripts/backfill-tournament-tiers.ts
*
* Retroactively calculates and sets tiers for all finalized tournaments,
* then populates series tier history based on those tiers.

View File

@@ -57,7 +57,7 @@ for (const dbPath of DB_FILES) {
if (hasErrors) {
console.error(
"\nRun `npm run test:e2e:generate-seeds` to regenerate test databases.",
"\nRun `pnpm run test:e2e:generate-seeds` to regenerate test databases.",
);
process.exit(1);
} else {

View File

@@ -1,4 +1,4 @@
// for testing use the command `npx tsx ./scripts/create-league-divisions.ts 6 'https://gist.githubusercontent.com/sendou-ink/38aa4d5d8426035ce178c09598ae627f/raw/17be9bb53a9f017c2097d0624f365d1c5a029f01/league.csv'`
// for testing use the command `pnpm exec tsx ./scripts/create-league-divisions.ts 6 'https://gist.githubusercontent.com/sendou-ink/38aa4d5d8426035ce178c09598ae627f/raw/17be9bb53a9f017c2097d0624f365d1c5a029f01/league.csv'`
import "dotenv/config";
import { z } from "zod";

View File

@@ -42,7 +42,7 @@ async function generatePreSeededDatabases() {
fs.copyFileSync(baseDbPath, outputPath);
execSync(
`npx vite-node scripts/seed-single-variation.ts -- ${variation} ${outputPath}`,
`pnpm exec vite-node scripts/seed-single-variation.ts -- ${variation} ${outputPath}`,
{ stdio: "inherit" },
);

View File

@@ -2,7 +2,7 @@
/**
* Tournament Tiering Experiment Script
*
* Run with: npx tsx scripts/tournament-tiers-experiment.ts
* Run with: pnpm exec tsx scripts/tournament-tiers-experiment.ts
*
* Calculates tournament tiers based on top teams' average SeedingSkill.
* Tweak the THRESHOLDS object to experiment with different tier distributions.

View File

@@ -1,4 +1,4 @@
// usage: npx tsx ./scripts/unlock-league-matches.ts <parentTournamentId>
// usage: pnpm exec tsx ./scripts/unlock-league-matches.ts <parentTournamentId>
import "dotenv/config";
import { sql } from "~/db/sql";
import invariant from "~/utils/invariant";