Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caebefdce8 | ||
|
|
a5fac5c75c | ||
|
|
9c71092278 | ||
|
|
5310463bec | ||
|
|
4eb49d1d44 | ||
|
|
a085de816f | ||
|
|
72db1f4a2c | ||
|
|
1e17278f94 | ||
|
|
f8ca248844 | ||
|
|
4adcc09643 | ||
|
|
e06727160c | ||
|
|
98384d264e | ||
|
|
e73191a729 | ||
|
|
327aee0b4b | ||
|
|
3353bcadc2 | ||
|
|
f92f7b592a | ||
|
|
dd1104310f |
26
CHANGELOG.md
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
## [2.3.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.3.0...v2.3.1) (2025-10-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing DOMPoint properties to getPointAtLength mock ([1e17278](https://github.com/antialias/soroban-abacus-flashcards/commit/1e17278f942b3fbcc5d05be746178f2e780f0bd9))
|
||||
* add missing name property to Passenger test mocks ([f8ca248](https://github.com/antialias/soroban-abacus-flashcards/commit/f8ca2488447e89151085942f708f6acf350a2747))
|
||||
* add non-null assertions to skillConfiguration utilities ([9c71092](https://github.com/antialias/soroban-abacus-flashcards/commit/9c7109227822884d25f8546739c80c6e7491e28d))
|
||||
* add optional chaining to stepBeadHighlights access ([a5fac5c](https://github.com/antialias/soroban-abacus-flashcards/commit/a5fac5c75c8cd67b218a5fd5ad98818dad74ab67))
|
||||
* add showAsAbacus property to ComplementQuestion type ([4adcc09](https://github.com/antialias/soroban-abacus-flashcards/commit/4adcc096430fbb03f0a8b2f0aef4be239aff9cd0))
|
||||
* add userId to optimistic player in useCreatePlayer ([5310463](https://github.com/antialias/soroban-abacus-flashcards/commit/5310463becd0974291cff49522ae5669a575410d))
|
||||
* change TypeScript moduleResolution from bundler to node ([327aee0](https://github.com/antialias/soroban-abacus-flashcards/commit/327aee0b4b5c0b0b2bf3eeb48d861bb3068f6127))
|
||||
* convert Jest mocks to Vitest in useSteamJourney tests ([e067271](https://github.com/antialias/soroban-abacus-flashcards/commit/e06727160c70a1ab38a003104d1fef8fb83ff92d))
|
||||
* convert player IDs from number to string in arcade tests ([72db1f4](https://github.com/antialias/soroban-abacus-flashcards/commit/72db1f4a2c3f930025cd5ced3fcf7c810dcc569d))
|
||||
* rewrite layout.nav.test to match actual RootLayout props ([a085de8](https://github.com/antialias/soroban-abacus-flashcards/commit/a085de816fcdeb055addabb8aec391b111cb5f94))
|
||||
* update useArcadeGuard tests with proper useViewerId mock ([4eb49d1](https://github.com/antialias/soroban-abacus-flashcards/commit/4eb49d1d44e1d85526ef6564f88a8fbcebffb4d2))
|
||||
* use Object.defineProperty for NODE_ENV in middleware tests ([e73191a](https://github.com/antialias/soroban-abacus-flashcards/commit/e73191a7298dbb6dd15da594267ea6221062c36b))
|
||||
* wrap Buffer in Uint8Array for Next.js Response API ([98384d2](https://github.com/antialias/soroban-abacus-flashcards/commit/98384d264e4a10d1836aa9f2e69151b122ffa7b0))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add explicit package.json script references to regime docs ([3353bca](https://github.com/antialias/soroban-abacus-flashcards/commit/3353bcadc2849104248c624973274ed90b86722a))
|
||||
* establish mandatory code quality regime for Claude Code ([dd11043](https://github.com/antialias/soroban-abacus-flashcards/commit/dd1104310f4e0e85640730ea0e96e4adda4bc505))
|
||||
* expand quality regime to define "done" for all work ([f92f7b5](https://github.com/antialias/soroban-abacus-flashcards/commit/f92f7b592af38ba9d0f5b1db3a061d63d92a5093))
|
||||
|
||||
## [2.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.2.1...v2.3.0) (2025-10-07)
|
||||
|
||||
|
||||
|
||||
86
apps/web/.claude/CLAUDE.md
Normal file
86
apps/web/.claude/CLAUDE.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Claude Code Instructions for apps/web
|
||||
|
||||
## MANDATORY: Quality Checks for ALL Work
|
||||
|
||||
**BEFORE declaring ANY work complete, fixed, or working**, you MUST run and pass these checks:
|
||||
|
||||
### When This Applies
|
||||
- Before every commit
|
||||
- Before saying "it's done" or "it's fixed"
|
||||
- Before marking a task as complete
|
||||
- Before telling the user something is working
|
||||
- After any code changes, no matter how small
|
||||
|
||||
```bash
|
||||
npm run pre-commit
|
||||
```
|
||||
|
||||
This single command runs all quality checks in the correct order:
|
||||
1. `npm run type-check` - TypeScript type checking (must have 0 errors)
|
||||
2. `npm run format` - Auto-format all code with Biome
|
||||
3. `npm run lint:fix` - Auto-fix linting issues with Biome + ESLint
|
||||
4. `npm run lint` - Verify 0 errors, 0 warnings
|
||||
|
||||
**DO NOT COMMIT** until all checks pass with zero errors and zero warnings.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
```bash
|
||||
npm run type-check # TypeScript: tsc --noEmit
|
||||
npm run format # Biome: format all files
|
||||
npm run format:check # Biome: check formatting without fixing
|
||||
npm run lint # Biome + ESLint: check for errors/warnings
|
||||
npm run lint:fix # Biome + ESLint: auto-fix issues
|
||||
npm run check # Biome: full check (format + lint + imports)
|
||||
npm run pre-commit # Run all checks (type + format + lint)
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
When asked to make ANY changes:
|
||||
|
||||
1. Make your code changes
|
||||
2. Run `npm run pre-commit`
|
||||
3. If it fails, fix the issues and run again
|
||||
4. Only after all checks pass can you:
|
||||
- Say the work is "done" or "complete"
|
||||
- Mark tasks as finished
|
||||
- Create commits
|
||||
- Tell the user it's working
|
||||
5. Push immediately after committing
|
||||
|
||||
**Nothing is complete until `npm run pre-commit` passes.**
|
||||
|
||||
## Details
|
||||
|
||||
See `.claude/CODE_QUALITY_REGIME.md` for complete documentation.
|
||||
|
||||
## No Pre-Commit Hooks
|
||||
|
||||
This project does not use git pre-commit hooks for religious reasons.
|
||||
You (Claude Code) are responsible for enforcing code quality before commits.
|
||||
|
||||
## Quick Reference: package.json Scripts
|
||||
|
||||
**Primary workflow:**
|
||||
```bash
|
||||
npm run pre-commit # ← Use this before every commit
|
||||
```
|
||||
|
||||
**Individual checks (if needed):**
|
||||
```bash
|
||||
npm run type-check # TypeScript: tsc --noEmit
|
||||
npm run format # Biome: format code (--write)
|
||||
npm run lint # Biome + ESLint: check only
|
||||
npm run lint:fix # Biome + ESLint: auto-fix
|
||||
```
|
||||
|
||||
**Additional tools:**
|
||||
```bash
|
||||
npm run format:check # Check formatting without changing files
|
||||
npm run check # Biome check (format + lint + organize imports)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Remember: Always run `npm run pre-commit` before creating commits.**
|
||||
143
apps/web/.claude/CODE_QUALITY_REGIME.md
Normal file
143
apps/web/.claude/CODE_QUALITY_REGIME.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Code Quality Regime
|
||||
|
||||
**MANDATORY**: Before declaring ANY work complete, fixed, or working, Claude MUST run these checks and fix all issues.
|
||||
|
||||
## Definition of "Done"
|
||||
|
||||
Work is NOT complete until:
|
||||
- ✅ All TypeScript errors are fixed (0 errors)
|
||||
- ✅ All code is formatted with Biome
|
||||
- ✅ All linting passes (0 errors, 0 warnings)
|
||||
- ✅ `npm run pre-commit` exits successfully
|
||||
|
||||
**Until these checks pass, the work is considered incomplete.**
|
||||
|
||||
## Quality Check Checklist (Always Required)
|
||||
|
||||
Run these before:
|
||||
- Committing code
|
||||
- Saying work is "done" or "complete"
|
||||
- Marking tasks as finished
|
||||
- Telling the user something is "working" or "fixed"
|
||||
|
||||
Run these commands in order. All must pass with 0 errors and 0 warnings:
|
||||
|
||||
```bash
|
||||
# 1. Type check
|
||||
npm run type-check
|
||||
|
||||
# 2. Format code
|
||||
npm run format
|
||||
|
||||
# 3. Lint and fix
|
||||
npm run lint:fix
|
||||
|
||||
# 4. Verify clean state
|
||||
npm run lint && npm run type-check
|
||||
```
|
||||
|
||||
## Quick Command (Run All Checks)
|
||||
|
||||
```bash
|
||||
npm run pre-commit
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
```json
|
||||
"pre-commit": "npm run type-check && npm run format && npm run lint:fix && npm run lint"
|
||||
```
|
||||
|
||||
This single command runs:
|
||||
1. `npm run type-check` → `tsc --noEmit` (TypeScript errors)
|
||||
2. `npm run format` → `npx @biomejs/biome format . --write` (auto-format)
|
||||
3. `npm run lint:fix` → `npx @biomejs/biome lint . --write && npx eslint . --fix` (auto-fix)
|
||||
4. `npm run lint` → `npx @biomejs/biome lint . && npx eslint .` (verify clean)
|
||||
|
||||
Fails fast if any step fails.
|
||||
|
||||
## The Regime Rules
|
||||
|
||||
### 1. TypeScript Errors: ZERO TOLERANCE
|
||||
- Run `npm run type-check` before every commit
|
||||
- Fix ALL TypeScript errors
|
||||
- No `@ts-ignore` or `@ts-expect-error` without explicit justification
|
||||
|
||||
### 2. Formatting: AUTOMATIC
|
||||
- Run `npm run format` before every commit
|
||||
- Biome handles all formatting automatically
|
||||
- Never commit unformatted code
|
||||
|
||||
### 3. Linting: ZERO ERRORS, ZERO WARNINGS
|
||||
- Run `npm run lint:fix` to auto-fix issues
|
||||
- Then run `npm run lint` to verify 0 errors, 0 warnings
|
||||
- Fix any remaining issues manually
|
||||
|
||||
### 4. Commit Order
|
||||
1. Make code changes
|
||||
2. Run `npm run pre-commit`
|
||||
3. If any check fails, fix and repeat
|
||||
4. Only commit when all checks pass
|
||||
5. Push immediately after commit
|
||||
|
||||
## Why No Pre-Commit Hooks?
|
||||
|
||||
This project intentionally avoids pre-commit hooks due to religious constraints.
|
||||
Instead, Claude Code is responsible for enforcing this regime through:
|
||||
|
||||
1. **This documentation** - Always visible and reference-able
|
||||
2. **Package.json scripts** - Easy to run checks
|
||||
3. **Session persistence** - This file lives in `.claude/` and is read by every session
|
||||
|
||||
## For Claude Code Sessions
|
||||
|
||||
**READ THIS FILE AT THE START OF EVERY SESSION WHERE YOU WILL COMMIT CODE**
|
||||
|
||||
When asked to commit:
|
||||
1. Check if you've run `npm run pre-commit` (or all 4 steps individually)
|
||||
2. If not, STOP and run the checks first
|
||||
3. Fix all issues before proceeding with the commit
|
||||
4. Only create commits when all checks pass
|
||||
|
||||
## Complete Scripts Reference
|
||||
|
||||
From `apps/web/package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"format": "npx @biomejs/biome format . --write",
|
||||
"format:check": "npx @biomejs/biome format .",
|
||||
"lint": "npx @biomejs/biome lint . && npx eslint .",
|
||||
"lint:fix": "npx @biomejs/biome lint . --write && npx eslint . --fix",
|
||||
"check": "npx @biomejs/biome check .",
|
||||
"pre-commit": "npm run type-check && npm run format && npm run lint:fix && npm run lint"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tools used:**
|
||||
- TypeScript: `tsc --noEmit` (type checking only, no output)
|
||||
- Biome: Fast formatter + linter (Rust-based, 10-100x faster than Prettier)
|
||||
- ESLint: React Hooks rules only (`rules-of-hooks` validation)
|
||||
|
||||
## Emergency Override
|
||||
|
||||
If you absolutely MUST commit with failing checks:
|
||||
1. Document WHY in the commit message
|
||||
2. Create a follow-up task to fix the issues
|
||||
3. Only use for emergency hotfixes
|
||||
|
||||
## Verification
|
||||
|
||||
After following this regime, you should see:
|
||||
```
|
||||
✓ Type check passed (0 errors)
|
||||
✓ Formatting applied
|
||||
✓ Linting passed (0 errors, 0 warnings)
|
||||
✓ Ready to commit
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**This regime is non-negotiable. Every commit must pass these checks.**
|
||||
@@ -74,7 +74,10 @@ describe('Middleware E2E', () => {
|
||||
|
||||
it('sets secure flag in production', async () => {
|
||||
const originalEnv = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'production'
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: 'production',
|
||||
configurable: true
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/')
|
||||
const res = await middleware(req)
|
||||
@@ -82,12 +85,18 @@ describe('Middleware E2E', () => {
|
||||
const cookie = res.cookies.get(GUEST_COOKIE_NAME)
|
||||
expect(cookie?.secure).toBe(true)
|
||||
|
||||
process.env.NODE_ENV = originalEnv
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: originalEnv,
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
|
||||
it('does not set secure flag in development', async () => {
|
||||
const originalEnv = process.env.NODE_ENV
|
||||
process.env.NODE_ENV = 'development'
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: 'development',
|
||||
configurable: true
|
||||
})
|
||||
|
||||
const req = new NextRequest('http://localhost:3000/')
|
||||
const res = await middleware(req)
|
||||
@@ -95,7 +104,10 @@ describe('Middleware E2E', () => {
|
||||
const cookie = res.cookies.get(GUEST_COOKIE_NAME)
|
||||
expect(cookie?.secure).toBe(false)
|
||||
|
||||
process.env.NODE_ENV = originalEnv
|
||||
Object.defineProperty(process.env, 'NODE_ENV', {
|
||||
value: originalEnv,
|
||||
configurable: true
|
||||
})
|
||||
})
|
||||
|
||||
it('sets maxAge correctly', async () => {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"format": "npx @biomejs/biome format . --write",
|
||||
"format:check": "npx @biomejs/biome format .",
|
||||
"check": "npx @biomejs/biome check .",
|
||||
"pre-commit": "npm run type-check && npm run format && npm run lint:fix && npm run lint",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"type-check": "tsc --noEmit",
|
||||
|
||||
@@ -1,54 +1,33 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import RootLayout from '../layout'
|
||||
|
||||
// Mock AppNavBar to verify it receives the nav prop
|
||||
const MockAppNavBar = ({ navSlot }: { navSlot?: React.ReactNode }) => (
|
||||
<div data-testid="app-nav-bar">
|
||||
{navSlot && <div data-testid="nav-slot-content">{navSlot}</div>}
|
||||
</div>
|
||||
)
|
||||
|
||||
jest.mock('../../components/AppNavBar', () => ({
|
||||
AppNavBar: MockAppNavBar,
|
||||
// Mock ClientProviders
|
||||
vi.mock('../../components/ClientProviders', () => ({
|
||||
ClientProviders: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="client-providers">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock all context providers
|
||||
jest.mock('../../contexts/AbacusDisplayContext', () => ({
|
||||
AbacusDisplayProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../contexts/UserProfileContext', () => ({
|
||||
UserProfileProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../contexts/GameModeContext', () => ({
|
||||
GameModeProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
jest.mock('../../contexts/FullscreenContext', () => ({
|
||||
FullscreenProvider: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
describe('RootLayout with nav slot', () => {
|
||||
it('passes nav slot to AppNavBar', () => {
|
||||
const navContent = <div>Memory Lightning</div>
|
||||
describe('RootLayout', () => {
|
||||
it('renders children with ClientProviders', () => {
|
||||
const pageContent = <div>Page content</div>
|
||||
|
||||
render(<RootLayout nav={navContent}>{pageContent}</RootLayout>)
|
||||
render(<RootLayout>{pageContent}</RootLayout>)
|
||||
|
||||
expect(screen.getByTestId('app-nav-bar')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('nav-slot-content')).toBeInTheDocument()
|
||||
expect(screen.getByText('Memory Lightning')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('client-providers')).toBeInTheDocument()
|
||||
expect(screen.getByText('Page content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('works without nav slot', () => {
|
||||
const pageContent = <div>Page content</div>
|
||||
it('renders html and body tags', () => {
|
||||
const pageContent = <div>Test content</div>
|
||||
|
||||
render(<RootLayout nav={null}>{pageContent}</RootLayout>)
|
||||
const { container } = render(<RootLayout>{pageContent}</RootLayout>)
|
||||
|
||||
expect(screen.getByTestId('app-nav-bar')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('nav-slot-content')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('Page content')).toBeInTheDocument()
|
||||
const html = container.querySelector('html')
|
||||
const body = container.querySelector('body')
|
||||
|
||||
expect(html).toBeInTheDocument()
|
||||
expect(html).toHaveAttribute('lang', 'en')
|
||||
expect(body).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ export async function GET(_request: NextRequest, { params }: { params: { id: str
|
||||
console.log('✅ Asset found, serving download')
|
||||
|
||||
// Return file with appropriate headers
|
||||
return new NextResponse(asset.data, {
|
||||
return new NextResponse(new Uint8Array(asset.data), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': asset.mimeType,
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function GET(_request: NextRequest, { params }: { params: { id: str
|
||||
headers.set('Content-Length', asset.data.length.toString())
|
||||
headers.set('Cache-Control', 'no-cache, no-store, must-revalidate')
|
||||
|
||||
return new NextResponse(asset.data, {
|
||||
return new NextResponse(new Uint8Array(asset.data), {
|
||||
status: 200,
|
||||
headers,
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function POST(request: NextRequest) {
|
||||
const filename = `soroban-flashcards-${config.range || 'cards'}.pdf`
|
||||
|
||||
// Return PDF directly as download
|
||||
return new NextResponse(pdfBuffer, {
|
||||
return new NextResponse(new Uint8Array(pdfBuffer), {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="${filename}"`,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type DisembarkingAnimation,
|
||||
usePassengerAnimations,
|
||||
} from '../../hooks/usePassengerAnimations'
|
||||
import type { ComplementQuestion } from '../../lib/gameTypes'
|
||||
import { useSteamJourney } from '../../hooks/useSteamJourney'
|
||||
import { useTrackManagement } from '../../hooks/useTrackManagement'
|
||||
import { useTrainTransforms } from '../../hooks/useTrainTransforms'
|
||||
@@ -80,7 +81,7 @@ interface SteamTrainJourneyProps {
|
||||
trainPosition: number
|
||||
pressure: number
|
||||
elapsedTime: number
|
||||
currentQuestion: { number: number; targetSum: number; correctAnswer: number } | null
|
||||
currentQuestion: ComplementQuestion | null
|
||||
currentInput: string
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('GameHUD', () => {
|
||||
|
||||
const mockPassenger: Passenger = {
|
||||
id: 'passenger-1',
|
||||
name: 'Test Passenger',
|
||||
avatar: '👨',
|
||||
originStationId: 'station-1',
|
||||
destinationStationId: 'station-2',
|
||||
@@ -49,6 +50,7 @@ describe('GameHUD', () => {
|
||||
number: 3,
|
||||
targetSum: 10,
|
||||
correctAnswer: 7,
|
||||
showAsAbacus: false,
|
||||
},
|
||||
currentInput: '7',
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('usePassengerAnimations', () => {
|
||||
// Create mock passengers
|
||||
mockPassenger1 = {
|
||||
id: 'passenger-1',
|
||||
name: 'Passenger 1',
|
||||
avatar: '👨',
|
||||
originStationId: 'station-1',
|
||||
destinationStationId: 'station-2',
|
||||
@@ -54,6 +55,7 @@ describe('usePassengerAnimations', () => {
|
||||
|
||||
mockPassenger2 = {
|
||||
id: 'passenger-2',
|
||||
name: 'Passenger 2',
|
||||
avatar: '👩',
|
||||
originStationId: 'station-1',
|
||||
destinationStationId: 'station-2',
|
||||
|
||||
@@ -15,9 +15,9 @@ import type { Passenger, Station } from '../../lib/gameTypes'
|
||||
import { useSteamJourney } from '../useSteamJourney'
|
||||
|
||||
// Mock sound effects
|
||||
jest.mock('../useSoundEffects', () => ({
|
||||
vi.mock('../useSoundEffects', () => ({
|
||||
useSoundEffects: () => ({
|
||||
playSound: jest.fn(),
|
||||
playSound: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -53,12 +53,12 @@ const _testStations: Station[] = [
|
||||
|
||||
describe('useSteamJourney - Passenger Boarding', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers()
|
||||
jest.useRealTimers()
|
||||
vi.runOnlyPendingTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
test('passenger boards when train reaches their origin station', () => {
|
||||
@@ -106,7 +106,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
|
||||
// Advance timers to trigger the interval
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// Verify passenger boarded
|
||||
@@ -150,7 +150,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
|
||||
// Advance timers
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// All three passengers should board (one per car)
|
||||
@@ -190,7 +190,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
pressure: 120,
|
||||
elapsedTime: 1000 + pos * 50,
|
||||
})
|
||||
jest.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(50)
|
||||
})
|
||||
|
||||
// Check if passenger boarded
|
||||
@@ -239,7 +239,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// p2 should board (on car 1 since car 0 is occupied)
|
||||
@@ -282,7 +282,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// Passenger should be delivered
|
||||
|
||||
@@ -17,7 +17,11 @@ describe('useTrackManagement - Passenger Display', () => {
|
||||
mockPath.getPointAtLength = vi.fn((distance: number) => ({
|
||||
x: distance,
|
||||
y: 300,
|
||||
}))
|
||||
w: 1,
|
||||
z: 0,
|
||||
matrixTransform: () => new DOMPoint(),
|
||||
toJSON: () => ({ x: distance, y: 300, w: 1, z: 0 }),
|
||||
})) as any
|
||||
mockPathRef = { current: mockPath }
|
||||
|
||||
// Mock track generator
|
||||
|
||||
@@ -25,7 +25,11 @@ describe('useTrackManagement', () => {
|
||||
mockPath.getPointAtLength = vi.fn((distance: number) => ({
|
||||
x: distance,
|
||||
y: 300,
|
||||
}))
|
||||
w: 1,
|
||||
z: 0,
|
||||
matrixTransform: () => new DOMPoint(),
|
||||
toJSON: () => ({ x: distance, y: 300, w: 1, z: 0 }),
|
||||
})) as any
|
||||
mockPathRef = { current: mockPath }
|
||||
|
||||
// Mock track generator
|
||||
@@ -52,6 +56,7 @@ describe('useTrackManagement', () => {
|
||||
mockPassengers = [
|
||||
{
|
||||
id: 'passenger-1',
|
||||
name: 'Passenger 1',
|
||||
avatar: '👨',
|
||||
originStationId: 'station-1',
|
||||
destinationStationId: 'station-2',
|
||||
@@ -73,6 +78,8 @@ describe('useTrackManagement', () => {
|
||||
pathRef: mockPathRef,
|
||||
stations: mockStations,
|
||||
passengers: mockPassengers,
|
||||
maxCars: 3,
|
||||
carSpacing: 7,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -90,6 +97,8 @@ describe('useTrackManagement', () => {
|
||||
pathRef: mockPathRef,
|
||||
stations: mockStations,
|
||||
passengers: mockPassengers,
|
||||
maxCars: 3,
|
||||
carSpacing: 7,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -107,6 +116,8 @@ describe('useTrackManagement', () => {
|
||||
pathRef: mockPathRef,
|
||||
stations: mockStations,
|
||||
passengers: mockPassengers,
|
||||
maxCars: 3,
|
||||
carSpacing: 7,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -123,6 +134,8 @@ describe('useTrackManagement', () => {
|
||||
pathRef: mockPathRef,
|
||||
stations: mockStations,
|
||||
passengers: mockPassengers,
|
||||
maxCars: 3,
|
||||
carSpacing: 7,
|
||||
})
|
||||
)
|
||||
|
||||
@@ -233,6 +246,7 @@ describe('useTrackManagement', () => {
|
||||
const newPassengers: Passenger[] = [
|
||||
{
|
||||
id: 'passenger-2',
|
||||
name: 'Passenger 2',
|
||||
avatar: '👩',
|
||||
originStationId: 'station-1',
|
||||
destinationStationId: 'station-2',
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type DisembarkingAnimation,
|
||||
usePassengerAnimations,
|
||||
} from '../../hooks/usePassengerAnimations'
|
||||
import type { ComplementQuestion } from '../../lib/gameTypes'
|
||||
import { useSteamJourney } from '../../hooks/useSteamJourney'
|
||||
import { useTrackManagement } from '../../hooks/useTrackManagement'
|
||||
import { useTrainTransforms } from '../../hooks/useTrainTransforms'
|
||||
@@ -80,7 +81,7 @@ interface SteamTrainJourneyProps {
|
||||
trainPosition: number
|
||||
pressure: number
|
||||
elapsedTime: number
|
||||
currentQuestion: { number: number; targetSum: number; correctAnswer: number } | null
|
||||
currentQuestion: ComplementQuestion | null
|
||||
currentInput: string
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('GameHUD', () => {
|
||||
number: 3,
|
||||
targetSum: 10,
|
||||
correctAnswer: 7,
|
||||
showAsAbacus: false,
|
||||
},
|
||||
currentInput: '7',
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import type { Passenger, Station } from '../../lib/gameTypes'
|
||||
import { useSteamJourney } from '../useSteamJourney'
|
||||
|
||||
// Mock sound effects
|
||||
jest.mock('../useSoundEffects', () => ({
|
||||
vi.mock('../useSoundEffects', () => ({
|
||||
useSoundEffects: () => ({
|
||||
playSound: jest.fn(),
|
||||
playSound: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -53,12 +53,12 @@ const _testStations: Station[] = [
|
||||
|
||||
describe('useSteamJourney - Passenger Boarding', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers()
|
||||
jest.useRealTimers()
|
||||
vi.runOnlyPendingTimers()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
test('passenger boards when train reaches their origin station', () => {
|
||||
@@ -106,7 +106,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
|
||||
// Advance timers to trigger the interval
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// Verify passenger boarded
|
||||
@@ -150,7 +150,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
|
||||
// Advance timers
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// All three passengers should board (one per car)
|
||||
@@ -190,7 +190,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
pressure: 120,
|
||||
elapsedTime: 1000 + pos * 50,
|
||||
})
|
||||
jest.advanceTimersByTime(50)
|
||||
vi.advanceTimersByTime(50)
|
||||
})
|
||||
|
||||
// Check if passenger boarded
|
||||
@@ -239,7 +239,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// p2 should board (on car 1 since car 0 is occupied)
|
||||
@@ -282,7 +282,7 @@ describe('useSteamJourney - Passenger Boarding', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(100)
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
|
||||
// Passenger should be delivered
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as nextNavigation from 'next/navigation'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useArcadeGuard } from '../useArcadeGuard'
|
||||
import * as arcadeSocket from '../useArcadeSocket'
|
||||
import * as viewerId from '../useViewerId'
|
||||
|
||||
// Mock Next.js navigation
|
||||
vi.mock('next/navigation', () => ({
|
||||
@@ -15,6 +16,11 @@ vi.mock('../useArcadeSocket', () => ({
|
||||
useArcadeSocket: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock useViewerId
|
||||
vi.mock('../useViewerId', () => ({
|
||||
useViewerId: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('useArcadeGuard', () => {
|
||||
const mockRouter = {
|
||||
push: vi.fn(),
|
||||
@@ -36,6 +42,11 @@ describe('useArcadeGuard', () => {
|
||||
vi.spyOn(nextNavigation, 'useRouter').mockReturnValue(mockRouter as any)
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockReturnValue(mockUseArcadeSocket)
|
||||
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
|
||||
data: 'test-user',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any)
|
||||
global.fetch = vi.fn()
|
||||
})
|
||||
|
||||
@@ -45,11 +56,7 @@ describe('useArcadeGuard', () => {
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
expect(result.current.hasActiveSession).toBe(false)
|
||||
@@ -68,11 +75,7 @@ describe('useArcadeGuard', () => {
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
@@ -100,11 +103,7 @@ describe('useArcadeGuard', () => {
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
|
||||
renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouter.push).toHaveBeenCalledWith('/arcade/memory-quiz')
|
||||
@@ -125,11 +124,7 @@ describe('useArcadeGuard', () => {
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
|
||||
renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalled()
|
||||
@@ -144,11 +139,7 @@ describe('useArcadeGuard', () => {
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
@@ -173,12 +164,7 @@ describe('useArcadeGuard', () => {
|
||||
|
||||
vi.spyOn(nextNavigation, 'usePathname').mockReturnValue('/arcade/matching')
|
||||
|
||||
renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
onRedirect,
|
||||
})
|
||||
)
|
||||
renderHook(() => useArcadeGuard({ onRedirect }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onRedirect).toHaveBeenCalledWith('/arcade/memory-quiz')
|
||||
@@ -186,22 +172,19 @@ describe('useArcadeGuard', () => {
|
||||
})
|
||||
|
||||
it('should not fetch session when disabled', () => {
|
||||
renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
enabled: false,
|
||||
})
|
||||
)
|
||||
renderHook(() => useArcadeGuard({ enabled: false }))
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not fetch session when userId is null', () => {
|
||||
renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: null,
|
||||
})
|
||||
)
|
||||
it('should not fetch session when viewerId is null', () => {
|
||||
vi.spyOn(viewerId, 'useViewerId').mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any)
|
||||
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -212,11 +195,7 @@ describe('useArcadeGuard', () => {
|
||||
status: 404,
|
||||
})
|
||||
|
||||
renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUseArcadeSocket.joinSession).toHaveBeenCalledWith('test-user')
|
||||
@@ -227,7 +206,7 @@ describe('useArcadeGuard', () => {
|
||||
let onSessionStateCallback: ((data: any) => void) | null = null
|
||||
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
|
||||
onSessionStateCallback = events.onSessionState || null
|
||||
onSessionStateCallback = events?.onSessionState || null
|
||||
return mockUseArcadeSocket
|
||||
})
|
||||
|
||||
@@ -236,11 +215,7 @@ describe('useArcadeGuard', () => {
|
||||
status: 404,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
@@ -268,7 +243,7 @@ describe('useArcadeGuard', () => {
|
||||
let onSessionEndedCallback: (() => void) | null = null
|
||||
|
||||
vi.spyOn(arcadeSocket, 'useArcadeSocket').mockImplementation((events) => {
|
||||
onSessionEndedCallback = events.onSessionEnded || null
|
||||
onSessionEndedCallback = events?.onSessionEnded || null
|
||||
return mockUseArcadeSocket
|
||||
})
|
||||
|
||||
@@ -283,11 +258,7 @@ describe('useArcadeGuard', () => {
|
||||
json: async () => mockSession,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.hasActiveSession).toBe(true)
|
||||
@@ -305,11 +276,7 @@ describe('useArcadeGuard', () => {
|
||||
it('should handle fetch errors gracefully', async () => {
|
||||
;(global.fetch as any).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useArcadeGuard({
|
||||
userId: 'test-user',
|
||||
})
|
||||
)
|
||||
const { result } = renderHook(() => useArcadeGuard())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false)
|
||||
|
||||
@@ -110,6 +110,7 @@ export function useCreatePlayer() {
|
||||
...newPlayer,
|
||||
createdAt: new Date(),
|
||||
isActive: newPlayer.isActive ?? false,
|
||||
userId: 'temp-user', // Temporary userId, will be replaced by server response
|
||||
}
|
||||
queryClient.setQueryData<Player[]>(playerKeys.list(), [
|
||||
...previousPlayers,
|
||||
|
||||
@@ -45,12 +45,12 @@ describe('Arcade Session Integration', () => {
|
||||
difficulty: 6,
|
||||
turnTimer: 30,
|
||||
gamePhase: 'setup',
|
||||
currentPlayer: 1,
|
||||
currentPlayer: "1",
|
||||
matchedPairs: 0,
|
||||
totalPairs: 6,
|
||||
moves: 0,
|
||||
scores: {},
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
consecutiveMatches: {},
|
||||
gameStartTime: null,
|
||||
gameEndTime: null,
|
||||
@@ -67,7 +67,7 @@ describe('Arcade Session Integration', () => {
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState,
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
})
|
||||
|
||||
expect(session).toBeDefined()
|
||||
@@ -86,7 +86,7 @@ describe('Arcade Session Integration', () => {
|
||||
playerId: testUserId,
|
||||
timestamp: Date.now(),
|
||||
data: {
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -147,12 +147,12 @@ describe('Arcade Session Integration', () => {
|
||||
difficulty: 6,
|
||||
turnTimer: 30,
|
||||
gamePhase: 'playing',
|
||||
currentPlayer: 1,
|
||||
currentPlayer: "1",
|
||||
matchedPairs: 0,
|
||||
totalPairs: 6,
|
||||
moves: 0,
|
||||
scores: { 1: 0 },
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
consecutiveMatches: { 1: 0 },
|
||||
gameStartTime: Date.now(),
|
||||
gameEndTime: null,
|
||||
@@ -169,7 +169,7 @@ describe('Arcade Session Integration', () => {
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: playingState,
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
})
|
||||
|
||||
// First move: flip card 1
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('session-manager', () => {
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: {},
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
})
|
||||
|
||||
// Verify user lookup by guestId
|
||||
@@ -159,7 +159,7 @@ describe('session-manager', () => {
|
||||
gameName: 'matching',
|
||||
gameUrl: '/arcade/matching',
|
||||
initialState: {},
|
||||
activePlayers: [1],
|
||||
activePlayers: ["1"],
|
||||
})
|
||||
|
||||
// Verify user was created
|
||||
|
||||
@@ -49,9 +49,9 @@ const ProgressiveTestComponent: React.FC<{
|
||||
|
||||
stepIndices.forEach((stepIndex, i) => {
|
||||
const description = fullInstruction.multiStepInstructions?.[i] || `Step ${i + 1}`
|
||||
const stepBeads = fullInstruction.stepBeadHighlights.filter(
|
||||
const stepBeads = fullInstruction.stepBeadHighlights?.filter(
|
||||
(bead) => bead.stepIndex === stepIndex
|
||||
)
|
||||
) || []
|
||||
|
||||
// Calculate the value change for this step by applying all bead movements
|
||||
let valueChange = 0
|
||||
|
||||
@@ -123,11 +123,11 @@ export function skillConfigurationToSkillSets(config: SkillConfiguration): {
|
||||
}
|
||||
if (mode === 'target') {
|
||||
if (!target.basic) target.basic = {} as any
|
||||
target.basic[skill as keyof typeof required.basic] = true
|
||||
target.basic![skill as keyof typeof required.basic] = true
|
||||
}
|
||||
if (mode === 'forbidden') {
|
||||
if (!forbidden.basic) forbidden.basic = {} as any
|
||||
forbidden.basic[skill as keyof typeof required.basic] = true
|
||||
forbidden.basic![skill as keyof typeof required.basic] = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -138,11 +138,11 @@ export function skillConfigurationToSkillSets(config: SkillConfiguration): {
|
||||
}
|
||||
if (mode === 'target') {
|
||||
if (!target.fiveComplements) target.fiveComplements = {} as any
|
||||
target.fiveComplements[skill as keyof typeof required.fiveComplements] = true
|
||||
target.fiveComplements![skill as keyof typeof required.fiveComplements] = true
|
||||
}
|
||||
if (mode === 'forbidden') {
|
||||
if (!forbidden.fiveComplements) forbidden.fiveComplements = {} as any
|
||||
forbidden.fiveComplements[skill as keyof typeof required.fiveComplements] = true
|
||||
forbidden.fiveComplements![skill as keyof typeof required.fiveComplements] = true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -153,11 +153,11 @@ export function skillConfigurationToSkillSets(config: SkillConfiguration): {
|
||||
}
|
||||
if (mode === 'target') {
|
||||
if (!target.tenComplements) target.tenComplements = {} as any
|
||||
target.tenComplements[skill as keyof typeof required.tenComplements] = true
|
||||
target.tenComplements![skill as keyof typeof required.tenComplements] = true
|
||||
}
|
||||
if (mode === 'forbidden') {
|
||||
if (!forbidden.tenComplements) forbidden.tenComplements = {} as any
|
||||
forbidden.tenComplements[skill as keyof typeof required.tenComplements] = true
|
||||
forbidden.tenComplements![skill as keyof typeof required.tenComplements] = true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "2.3.0",
|
||||
"version": "2.3.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user