diff --git a/.github/workflows/test-app.yml b/.github/workflows/test-app.yml index 9b338851..0a9e073f 100644 --- a/.github/workflows/test-app.yml +++ b/.github/workflows/test-app.yml @@ -4,39 +4,60 @@ on: pull_request: jobs: - build: - name: Build Image Without Push + lint: + name: Lint runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache Docker layers - uses: actions/cache@v4 + - name: Setup Node.js + uses: actions/setup-node@v4 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- + node-version: 22 + cache: npm - - name: Build image (no push) - uses: docker/build-push-action@v5 + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 with: - context: ./ - push: false # Do NOT push - platforms: linux/amd64 - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new + node-version: 22 + cache: npm - - name: Move cache - if: steps.buildx.outcome == 'success' - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache + - name: Install dependencies + run: npm ci + + - name: Test + run: npm test + + build: + name: Build + needs: [lint, test] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build diff --git a/CLAUDE.md b/CLAUDE.md index 00e42077..1d5cffb3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,13 +20,13 @@ Node 22 required (see `mise.toml`). Deployed on Vercel. ## Git Workflow -Feature branches are named `issue-XXX` where `XXX` is the related GitHub issue number. +Feature branches are named `issue-XXX` where `XXX` is the related GitHub issue number. PR descriptions should not include a test plan section. ## CI/CD - `Dockerfile` — multi-stage Alpine build with standalone Next.js output, pushed to GHCR - `.github/workflows/build-app.yaml` — builds and pushes Docker image on push to master -- `.github/workflows/test-app.yml` — builds Docker image (no push) on PRs +- `.github/workflows/test-app.yml` — runs lint, tests, and build on PRs (build runs only after lint and tests pass) ## Code Style @@ -75,7 +75,14 @@ CSS Modules (`.module.css`) for component-scoped styles. Global theme via CSS cu ## TypeScript -Path alias: `@/*` → `src/*`. Strict mode enabled. Target ES6. `moduleDetection: "force"` in tsconfig (required because Next.js auto-generates `.next/types/validator.ts` without exports, which breaks `isolatedModules`). Both `@/` and `src/` import styles are used in the codebase — prefer `@/` for new code. +Path alias: `@/*` → `src/*`. Strict mode enabled. Target ES6. `moduleDetection: "force"` in tsconfig (required because Next.js auto-generates `.next/types/validator.ts` without exports, which breaks `verbatimModuleSyntax`). Use `@/` path alias for all local imports, not `src/`. Legacy `src/` imports exist in the codebase but should not be used in new or modified code. + +`verbatimModuleSyntax` is enabled — use `import type` for type-only imports. When importing both values and types from the same module, use separate `import` and `import type` statements: + +```ts +import { createPage } from "@/remote/factory" +import type { Page } from "@/types" +``` ## Accessibility diff --git a/pages/404.tsx b/pages/404.tsx index 13382355..c8af0613 100644 --- a/pages/404.tsx +++ b/pages/404.tsx @@ -1,4 +1,4 @@ -import { NextPage } from "next" +import type { NextPage } from "next" import { Wrapper } from "@/components/Wrapper" import { NotFound } from "@/components/NotFound" import Copyright from "@/components/Copyright" diff --git a/pages/[...slug].tsx b/pages/[...slug].tsx index 4331c2ca..1a5d94ea 100644 --- a/pages/[...slug].tsx +++ b/pages/[...slug].tsx @@ -1,11 +1,11 @@ import Head from "next/head" -import { App } from "src/components/App" -import { Page } from "src/components/Page" +import { App } from "@/components/App" +import { Page } from "@/components/Page" import { NextSeo } from "next-seo" -import { MetaBuilder } from "src/lib/meta" -import { GetStaticProps, NextPage } from "next" -import { IBreadcumbsPart, IMenu, IMeta, ITag, Sign, Token } from "@/types" -import { Html } from "src/components/Html" +import { MetaBuilder } from "@/lib/meta" +import type { GetStaticProps, NextPage } from "next" +import type { BreadcrumbPart, MenuItem, Meta, Tag, Sign, Token } from "@/types" +import { Html } from "@/components/Html" import { Youtube } from "@/components/Youtube" import { paramsToSlug } from "@/remote/lib" import { PageGrid } from "@/components/PageGrid" @@ -17,11 +17,11 @@ import { getMenu, getPageBySlug, getUrls } from "@/remote/api" type Props = { title: string - tags: ITag[] + tags: Tag[] date: string - breadcrumb: IBreadcumbsPart[] - meta?: IMeta - menu: IMenu[], + breadcrumb: BreadcrumbPart[] + meta?: Meta + menu: MenuItem[], tokens: Token[] documentSignature: Sign } diff --git a/pages/index.tsx b/pages/index.tsx index fdeccd0f..539727e3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,18 +1,18 @@ import Head from "next/head" -import { App } from "src/components/App" +import { App } from "@/components/App" import { NextSeo } from "next-seo" -import { PageGrid } from "src/components/PageGrid" -import { HudozkaTitle } from "src/components/HudozkaTitle" -import { MetaBuilder } from "src/lib/meta" -import { GetStaticProps, NextPage } from "next" -import { IMenu, IMeta, PageCardDto } from "src/types" +import { PageGrid } from "@/components/PageGrid" +import { HudozkaTitle } from "@/components/HudozkaTitle" +import { MetaBuilder } from "@/lib/meta" +import type { GetStaticProps, NextPage } from "next" +import type { MenuItem, Meta, PageCardDto } from "@/types" import { useMobile } from "@/hooks/useMobile" import { getHomeCards, getMenu } from "@/remote/api" type Props = { title: string - meta: IMeta - menu: IMenu[] + meta: Meta + menu: MenuItem[] items: PageCardDto[] } diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 364c3d8a..98f6c121 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -3,9 +3,9 @@ import { MenuContext } from "@/context/MenuContext" import { Footer } from "../Footer" import { Content } from "../Content" import { Breadcrumbs } from "../Breadcrumbs" -import { IBreadcumbsPart, IMenu } from "src/types" +import type { BreadcrumbPart, MenuItem } from "@/types" import { useRouter } from "next/router" -import { useMobile } from "src/hooks/useMobile" +import { useMobile } from "@/hooks/useMobile" import { Navigation } from "@/components/Navigation" import { Box } from "@/ui/Box" import { AccessibilityButton } from "../AccessibilityButton" @@ -29,8 +29,8 @@ const Gosuslugi = dynamic(import("../Gosuslugi").then(module => module.Gosuslugi export type AppProps = { showAuthor?: boolean contentStyle?: React.CSSProperties - breadcrumbs?: IBreadcumbsPart[] - menu: IMenu[] + breadcrumbs?: BreadcrumbPart[] + menu: MenuItem[] children?: React.ReactNode } diff --git a/src/components/Breadcrumbs/index.tsx b/src/components/Breadcrumbs/index.tsx index 7e3e0b3f..20b1708c 100644 --- a/src/components/Breadcrumbs/index.tsx +++ b/src/components/Breadcrumbs/index.tsx @@ -1,12 +1,12 @@ import s from "./styles.module.css" -import { IBreadcumbsPart } from "src/types" +import type { BreadcrumbPart } from "@/types" import { Button } from "@/ui/Button" -import { insertBetween } from "src/lib/array" +import { insertBetween } from "@/lib/array" export type BreadcrumbsProps = { style?: React.CSSProperties - items: IBreadcumbsPart[] + items: BreadcrumbPart[] path: string } diff --git a/src/components/File/index.tsx b/src/components/File/index.tsx index 6e921276..58ed5f02 100644 --- a/src/components/File/index.tsx +++ b/src/components/File/index.tsx @@ -1,6 +1,6 @@ import s from "./styles.module.css" -import { FileDefinition } from "src/types" +import type { FileDefinition } from "@/types" import { ext, size } from "src/lib/file" import Link from "next/link" import Image from "next/image" diff --git a/src/components/FileCard/Signature.tsx b/src/components/FileCard/Signature.tsx index 4d6a5096..626697a7 100644 --- a/src/components/FileCard/Signature.tsx +++ b/src/components/FileCard/Signature.tsx @@ -1,6 +1,6 @@ import s from "./signature.module.css" -import { Sign } from "@/types" +import type { Sign } from "@/types" import { HiOutlineShieldCheck } from "react-icons/hi" import * as Tooltip from "@radix-ui/react-tooltip" diff --git a/src/components/FileCard/index.tsx b/src/components/FileCard/index.tsx index b8d8ce16..35a1e566 100644 --- a/src/components/FileCard/index.tsx +++ b/src/components/FileCard/index.tsx @@ -2,7 +2,7 @@ import s from "./filecard.module.css" import Image from "next/image" import dynamic from "next/dynamic" -import { FileTokenData, Sign } from "@/types" +import type { FileTokenData, Sign } from "@/types" import { size, ext } from "@/lib/file" import { getResizedUrl } from "@/lib/image" import Link from "next/link" diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index f3a2827a..5ab8017a 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -1,6 +1,6 @@ import s from "./styles.module.css" -import { IMenu } from "@/types" +import type { MenuItem } from "@/types" import * as NavigationMenu from "@radix-ui/react-navigation-menu" import classnames from "classnames/bind" import { useRouter } from "next/router" @@ -10,8 +10,8 @@ let cx = classnames.bind(s) export type MenuProps = { vertical?: boolean - items: IMenu[] - onItemClick?: (item: IMenu) => void + items: MenuItem[] + onItemClick?: (item: MenuItem) => void } export const Menu: React.FC = ({ vertical = false, onItemClick, items }) => { diff --git a/src/components/Page/index.tsx b/src/components/Page/index.tsx index 38257e6d..f9b12daf 100644 --- a/src/components/Page/index.tsx +++ b/src/components/Page/index.tsx @@ -1,13 +1,13 @@ import s from "./styles.module.css" import { PageMeta } from "../PageMeta" -import { ITag } from "src/types" +import type { Tag } from "@/types" export type PageProps = { children?: React.ReactNode style?: React.CSSProperties date?: Date - tags: ITag[] + tags: Tag[] } export const Page: React.FC = props => ( diff --git a/src/components/PageGrid/index.tsx b/src/components/PageGrid/index.tsx index da97c313..dacb6a68 100644 --- a/src/components/PageGrid/index.tsx +++ b/src/components/PageGrid/index.tsx @@ -1,7 +1,8 @@ import Image from "next/image" -import { PageCardDto } from "@/types" +import type { PageCardDto } from "@/types" import { CardGrid } from "../CardGrid" -import { Card, CardLayout } from "../Card" +import { Card } from "../Card" +import type { CardLayout } from "../Card" import { Date } from "./Date" import { Spacer } from "../Spacer" import { Box } from "@/ui/Box" diff --git a/src/components/PageMeta/index.tsx b/src/components/PageMeta/index.tsx index a09bd89f..fc2a0274 100644 --- a/src/components/PageMeta/index.tsx +++ b/src/components/PageMeta/index.tsx @@ -1,15 +1,16 @@ import s from "./styles.module.css" import cx from "classnames" -import { dateFormat } from "src/lib/date" -import { ITag } from "src/types" -import { TagList, Direction } from "../TagList" -import { useMobile } from "src/hooks/useMobile" +import { dateFormat } from "@/lib/date" +import type { Tag } from "@/types" +import { TagList } from "../TagList" +import type { Direction } from "../TagList" +import { useMobile } from "@/hooks/useMobile" export type PageMetaProps = { style?: React.CSSProperties date?: Date - tags?: ITag[] + tags?: Tag[] } export const PageMeta: React.FC = props => { diff --git a/src/components/TagList/index.tsx b/src/components/TagList/index.tsx index 20c14833..1be90aae 100644 --- a/src/components/TagList/index.tsx +++ b/src/components/TagList/index.tsx @@ -1,7 +1,7 @@ import s from "./styles.module.css" import cx from "classnames" -import { ITag } from "src/types" +import type { Tag } from "@/types" import { Button } from "@/ui/Button" const directionClass = { @@ -11,13 +11,13 @@ const directionClass = { export type Direction = "horizontal" | "vertical" -export type TagProps = { +export type TagItemProps = { children?: React.ReactNode href: string direction: Direction } -export const Tag: React.FC = props => ( +export const TagItem: React.FC = props => (