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
13 changes: 12 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
"error",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
],
"@typescript-eslint/no-explicit-any": "warn"
"@typescript-eslint/no-explicit-any": "warn",
"no-restricted-imports": [
"error",
{
"patterns": [
{
"regex": "^\\$lib(/|$)",
"message": "SvelteKit 3 removes $lib. Use #lib with an explicit extension (e.g. #lib/types/task.js)."
}
]
}
]
}
}
2 changes: 1 addition & 1 deletion docs/guides/agent-rules/prisma-db.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ paths:

## Server-Only Code

- Import the DB client (`$lib/server/database`) only in `src/lib/server/**` and in server-only services under `src/lib/services/**` and `src/features/**/services/**`
- Import the DB client (`#lib/server/database.js`) only in `src/lib/server/**` and in server-only services under `src/lib/services/**` and `src/features/**/services/**`
- Never import server-only code in client components

## Service Layer
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/agent-rules/server-cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Services call `getCached*()` with a `fetchFn` that performs the DB query. The se

```typescript
// In service file
import { getCachedTasksMap } from '$lib/server/tasks/cache';
import { getCachedTasksMap } from '#lib/server/tasks/cache.js';

export async function getTasksByTaskId(): Promise<Map<string, Task>> {
return getCachedTasksMap(async () => {
Expand All @@ -91,7 +91,7 @@ export async function getTasksByTaskId(): Promise<Map<string, Task>> {
Mock cache in service tests to bypass caching:

```typescript
vi.mock('$lib/server/tasks/cache', () => ({
vi.mock('#lib/server/tasks/cache.js', () => ({
getCachedTasksMap: (fetchFn: () => Promise<unknown>) => fetchFn(),
invalidateTaskCaches: vi.fn(),
}));
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/agent-rules/testing-e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ paths:
## Setup

- File extension: **must** be `.spec.ts` (`.test.ts` not detected by `playwright.config.ts`)
- No path aliases (`$lib`, `$features` not resolved outside SvelteKit). Use type-only imports for types:
- No path aliases or subpath imports (`$features` is resolved only by Vite; `#lib/*.js` resolves to `src/lib/*.js`, but the Playwright runner does not map it to the `.ts` source). Use type-only imports for types:

```typescript
// Bad: runtime import fails in E2E
Expand Down
4 changes: 2 additions & 2 deletions docs/guides/agent-rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ test.each([TaskGrade.PENDING, TaskGrade.Q11, TaskGrade.Q10, TaskGrade.D6])(

### Service Layer (Prisma)

Mock with `vi.mock('$lib/server/database', ...)`. Use helpers:
Mock with `vi.mock('#lib/server/database.js', ...)`. Use helpers:

```typescript
const mockFindUnique = (data) => db.task.findUnique.mockResolvedValue(data);
Expand All @@ -95,7 +95,7 @@ afterAll(() => disposeDomainCaches());
beforeEach(() => invalidateDomainCaches());

// Bypass caching in service tests:
vi.mock('$lib/server/tasks/cache', () => ({
vi.mock('#lib/server/tasks/cache.js', () => ({
getCachedTasksMap: (fetchFn: () => Promise<unknown>) => fetchFn(),
invalidateTaskCaches: vi.fn(),
}));
Expand Down
11 changes: 7 additions & 4 deletions docs/guides/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ feature 内は技術的な役割で分ける。必要なディレクトリだけ
- `utils/`: 副作用のない関数。テストを隣接配置する。
- `stores/`: Svelte Runes を使う `.svelte.ts`。

`features/{feature}/server/` は `$lib/server/` と違い、SvelteKit が client import を自動では禁止しない。規約と推移的な server-only dependency により client から分離する。複数 feature で使うサーバコードは `src/lib/server/` へ移す。
`features/{feature}/server/` は `src/lib/server/` と違い、SvelteKit が client import を自動では禁止しない。規約と推移的な server-only dependency により client から分離する。複数 feature で使うサーバコードは `src/lib/server/` へ移す。

### 3. src/lib

Expand Down Expand Up @@ -139,12 +139,15 @@ include: [
];
```

### 6. path alias
### 6. path alias と subpath imports

`svelte.config.js` で `$lib` と `$features` を定義している。
`src/lib` は `package.json#imports` の `#lib/*` で参照する。SvelteKit 3 で組み込みの `$lib` が削除されるため `$lib` は使わず、oxlint と ESLint の `no-restricted-imports` で禁止している。
`#lib` には明示的な拡張子が必要で、TS モジュールは `.js`、コンポーネントは `.svelte`、runes モジュール(`.svelte.ts`)は `.svelte.js` と書く。`vi.mock` の specifier も同じ形にする。
lint が検出するのは import・export 宣言(`import type`・`export … from` を含む)と `.ts` の動的 `import()` で、`vi.mock('$lib/...')` と `.svelte` 内の動的 `import()` は検出しない。import の specifier をまとめて変えたときは、`rg -n '\$lib' src e2e prisma` で残りが無いことも確かめる。
`$features` は `svelte.config.js` の alias で定義している。

```typescript
import { formatDate } from '$lib/utils/format';
import { formatDate } from '#lib/utils/format.js';
import WorkBookForm from '$features/workbooks/components/shared/WorkBookForm.svelte';
import type { PageServerLoad } from './$types';
```
Expand Down
13 changes: 13 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ export default [
'svelte/no-useless-mustaches': 'warn',
'svelte/prefer-writable-derived': 'warn', // New in 3.6.0 - prefer $derived over $state+$effect
'svelte/valid-prop-names-in-kit-pages': 'warn', // Allow props other than data/errors in pages
// Kit 2 still resolves $lib, so new imports would only break on Kit 3; oxlint covers JS/TS files
'no-restricted-imports': [
'error',
{
patterns: [
{
regex: '^\\$lib(/|$)',
message:
'SvelteKit 3 removes $lib. Use #lib with an explicit extension (e.g. #lib/types/task.js).',
},
],
},
],
'no-unused-vars': 'off', // Use TypeScript version instead
'no-undef': 'off', // TypeScript handles this
},
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"zod": "4.6.7"
},
"type": "module",
"imports": {
"#lib/*": "./src/lib/*"
},
"dependencies": {
"@dnd-kit/helpers": "0.5.1",
"@dnd-kit/svelte": "0.5.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import { Input, Button, Alert, Checkbox, Modal } from 'flowbite-svelte';
import Info from '@lucide/svelte/icons/info';

import ContainerWrapper from '$lib/components/ContainerWrapper.svelte';
import FormWrapper from '$lib/components/FormWrapper.svelte';
import LabelWrapper from '$lib/components/LabelWrapper.svelte';
import ContainerWrapper from '#lib/components/ContainerWrapper.svelte';
import FormWrapper from '#lib/components/FormWrapper.svelte';
import LabelWrapper from '#lib/components/LabelWrapper.svelte';
import WarningMessageOnDeletingAccount from './WarningMessageOnDeletingAccount.svelte';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import { Label, Input, P, Clipboard } from 'flowbite-svelte';
import ClipboardCopy from '@lucide/svelte/icons/clipboard-copy';
import Check from '@lucide/svelte/icons/check';
import ContainerWrapper from '$lib/components/ContainerWrapper.svelte';
import FormWrapper from '$lib/components/FormWrapper.svelte';
import LabelWrapper from '$lib/components/LabelWrapper.svelte';
import SubmissionButton from '$lib/components/SubmissionButton.svelte';
import ContainerWrapper from '#lib/components/ContainerWrapper.svelte';
import FormWrapper from '#lib/components/FormWrapper.svelte';
import LabelWrapper from '#lib/components/LabelWrapper.svelte';
import SubmissionButton from '#lib/components/SubmissionButton.svelte';

interface Props {
username: string;
Expand Down
6 changes: 3 additions & 3 deletions src/features/account/services/atcoder_verification.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';

vi.mock('$lib/server/database', () => ({
vi.mock('#lib/server/database.js', () => ({
default: {
user: {
findUniqueOrThrow: vi.fn(),
Expand All @@ -13,13 +13,13 @@ vi.mock('$lib/server/database', () => ({
},
}));

vi.mock('$lib/utils/hash', () => ({
vi.mock('#lib/utils/hash.js', () => ({
sha256: vi.fn().mockResolvedValue('mocked-hash'),
}));

import { Roles, type AtCoderAccount } from '@prisma/client';

import prisma from '$lib/server/database';
import prisma from '#lib/server/database.js';
import { generate, validate, reset } from './atcoder_verification';

// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/features/account/services/atcoder_verification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { default as db } from '$lib/server/database';
import { sha256 } from '$lib/utils/hash';
import { default as db } from '#lib/server/database.js';
import { sha256 } from '#lib/utils/hash.js';

const EXTERNAL_API_TIMEOUT_MS = 5000;

Expand Down
4 changes: 2 additions & 2 deletions src/features/auth/server/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';

import { Prisma, Roles } from '@prisma/client';

vi.mock('$lib/server/database', () => ({
vi.mock('#lib/server/database.js', () => ({
default: {
session: {
create: vi.fn(),
Expand All @@ -13,7 +13,7 @@ vi.mock('$lib/server/database', () => ({
},
}));

import db from '$lib/server/database';
import db from '#lib/server/database.js';
import { createSession, validateSession, invalidateSession, SESSION_COOKIE_NAME } from './session';

const mockDb = db as unknown as {
Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/server/session.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
import type { Roles } from '@prisma/client';

import client from '$lib/server/database';
import client from '#lib/server/database.js';
import { generateRandomString } from './random';

// lucia v2 defaults: active 24h, idle +14d
Expand Down
6 changes: 3 additions & 3 deletions src/features/auth/services/admin_access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ vi.mock('@sveltejs/kit', () => {
return { redirect: vi.fn(redirectImpl), error: vi.fn(errorImpl) };
});

vi.mock('$lib/services/users', () => ({
vi.mock('#lib/services/users.js', () => ({
getUser: vi.fn(),
}));

import * as userService from '$lib/services/users';
import { Roles } from '$lib/types/user';
import * as userService from '#lib/services/users.js';
import { Roles } from '#lib/types/user.js';
import { validateAdminAccess, validateAdminAccessForApi } from './admin_access';

const createMockLocalsWithSession = (username = 'admin-user') =>
Expand Down
8 changes: 4 additions & 4 deletions src/features/auth/services/admin_access.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { redirect, error } from '@sveltejs/kit';

import * as userService from '$lib/services/users';
import * as userService from '#lib/services/users.js';

import { Roles } from '$lib/types/user';
import { Roles } from '#lib/types/user.js';

import { isAdmin } from '$lib/utils/authorship';
import { isAdmin } from '#lib/utils/authorship.js';
import {
TEMPORARY_REDIRECT,
UNAUTHORIZED,
FORBIDDEN,
} from '$lib/constants/http-response-status-codes';
} from '#lib/constants/http-response-status-codes.js';

import { buildLoginPath } from '../utils/login';

Expand Down
4 changes: 2 additions & 2 deletions src/features/auth/services/credentials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, test, expect, vi } from 'vitest';

import { Prisma } from '@prisma/client';

vi.mock('$lib/server/database', () => ({
vi.mock('#lib/server/database.js', () => ({
default: {
$transaction: vi.fn(),
user: {
Expand All @@ -20,7 +20,7 @@ vi.mock('../server/password', () => ({
verifyPassword: vi.fn(),
}));

import db from '$lib/server/database';
import db from '#lib/server/database.js';
import { hashPassword, verifyPassword } from '../server/password';
import { registerUser, authenticateUser } from './credentials';

Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/services/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';

import client from '$lib/server/database';
import client from '#lib/server/database.js';
import { hashPassword, verifyPassword } from '../server/password';
import { generateRandomString } from '../server/random';

Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/services/session_guards.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';

import { TEMPORARY_REDIRECT } from '$lib/constants/http-response-status-codes';
import { TEMPORARY_REDIRECT } from '#lib/constants/http-response-status-codes.js';

import { buildLoginPath } from '../utils/login';

Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/utils/login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LOGIN_PAGE } from '$lib/constants/navbar-links';
import { LOGIN_PAGE } from '#lib/constants/navbar-links.js';

/**
* Builds the login path with an optional redirect parameter.
Expand Down
2 changes: 1 addition & 1 deletion src/features/auth/utils/signup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SIGNUP_PAGE } from '$lib/constants/navbar-links';
import { SIGNUP_PAGE } from '#lib/constants/navbar-links.js';

/**
* Builds the signup path with an optional redirect parameter.
Expand Down
12 changes: 6 additions & 6 deletions src/features/tasks/components/contest-table/TaskTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
TableHeadCell,
} from 'flowbite-svelte';

import type { TaskResults, TaskResult } from '$lib/types/task';
import type { TaskResults, TaskResult } from '#lib/types/task.js';
import type {
ContestTableProvider,
ContestTableDisplayConfig,
ContestTableMetaData,
} from '$features/tasks/types/contest-table/contest_table_provider';
import type { ContestTaskPairKey } from '$lib/types/contest_task_pair';
import type { ContestTaskPairKey } from '#lib/types/contest_task_pair.js';
import type { VoteStatisticsMap } from '$features/votes/types/vote_result';

import TaskTableBodyCell from './TaskTableBodyCell.svelte';
Expand All @@ -31,11 +31,11 @@
type ContestTableProviderGroups,
} from '$features/tasks/utils/contest-table/contest_table_provider';

import { useLocalStorage } from '$lib/stores/local_storage_helper.svelte';
import { useLocalStorage } from '#lib/stores/local_storage_helper.svelte.js';

import { getBackgroundColorFrom } from '$lib/services/submission_status';
import { areAllTasksAccepted } from '$lib/utils/task';
import { createContestTaskPairKey } from '$lib/utils/contest_task_pair';
import { getBackgroundColorFrom } from '#lib/services/submission_status.js';
import { areAllTasksAccepted } from '#lib/utils/task.js';
import { createContestTaskPairKey } from '#lib/utils/contest_task_pair.js';
import { getBodyRowClasses } from './_utils/contest_table_layout';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<script lang="ts">
import type { TaskResult } from '$lib/types/task';
import type { TaskResult } from '#lib/types/task.js';
import type { VoteStatisticsMap } from '$features/votes/types/vote_result';

import VotableGrade from '@/features/votes/components/VotableGrade.svelte';
import ExternalLinkWrapper from '$lib/components/ExternalLinkWrapper.svelte';
import UpdatingDropdown from '$lib/components/SubmissionStatus/UpdatingDropdown.svelte';
import ExternalLinkWrapper from '#lib/components/ExternalLinkWrapper.svelte';
import UpdatingDropdown from '#lib/components/SubmissionStatus/UpdatingDropdown.svelte';

import { getTaskUrl, removeTaskIndexFromTitle } from '$lib/utils/task';
import { getTaskUrl, removeTaskIndexFromTitle } from '#lib/utils/task.js';
import { formatAojTitle } from '$features/tasks/utils/contest-table/aoj_labels';

interface Props {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TaskGrade, type TaskResult, type TaskResults } from '$lib/types/task';
import { TaskGrade, type TaskResult, type TaskResults } from '#lib/types/task.js';

// Default task result with minimal initialization.
// Most fields are empty strings as they're not relevant for these tests.
Expand Down
2 changes: 1 addition & 1 deletion src/features/tasks/stores/active_contest_type.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLocalStorage } from '$lib/stores/local_storage_helper.svelte';
import { useLocalStorage } from '#lib/stores/local_storage_helper.svelte.js';
import {
type ContestTableProviderGroups,
contestTableProviderGroups,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ContestType } from '$lib/contests/types/contest';
import type { TaskResults, TaskResult } from '$lib/types/task';
import type { ContestType } from '#lib/contests/types/contest.js';
import type { TaskResults, TaskResult } from '#lib/types/task.js';

/**
* Provider interface for building and managing contest tables.
Expand Down
4 changes: 2 additions & 2 deletions src/features/tasks/utils/contest-table/aalc_provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, test, expect } from 'vitest';

import { ContestType } from '$lib/contests/types/contest';
import type { TaskResults } from '$lib/types/task';
import { ContestType } from '#lib/contests/types/contest.js';
import type { TaskResults } from '#lib/types/task.js';

import { AALCProvider } from './aalc_provider';

Expand Down
4 changes: 2 additions & 2 deletions src/features/tasks/utils/contest-table/aalc_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {
type ContestTableMetaData,
type ContestTableDisplayConfig,
} from '$features/tasks/types/contest-table/contest_table_provider';
import type { TaskResult } from '$lib/types/task';
import type { TaskResult } from '#lib/types/task.js';

import { classifyContest } from '$lib/contests';
import { classifyContest } from '#lib/contests/index.js';

import { ContestTableProviderBase } from './contest_table_provider_base';

Expand Down
Loading
Loading