Agent Skillsgrowilabs/growi › app-specific-patterns

app-specific-patterns

GitHub

定义GROWI应用中Next.js、Jotai状态管理和SWR数据获取的代码规范与目录结构,并提供针对Next.js路由和Jotai的Vitest测试Mock模式。

apps/app/.claude/skills/app-specific-patterns/SKILL.md growilabs/growi

触发场景

在 apps/app 目录下进行开发 需要遵循 GROWI 应用特定的前端架构规范 编写涉及 Jotai 或 SWR 的组件代码

安装

npx skills add growilabs/growi --skill app-specific-patterns -g -y
更多选项

非标准路径

npx skills add https://github.com/growilabs/growi/tree/master/apps/app/.claude/skills/app-specific-patterns -g -y

不安装直接使用

npx skills use growilabs/growi@app-specific-patterns

指定 Agent (Claude Code)

npx skills add growilabs/growi --skill app-specific-patterns -a claude-code -g -y

安装 repo 全部 skill

npx skills add growilabs/growi --all -g -y

预览 repo 内 skill

npx skills add growilabs/growi --list

SKILL.md

Frontmatter
{
    "name": "app-specific-patterns",
    "description": "GROWI main application (apps\/app) specific patterns for Next.js, Jotai, SWR, and testing. Auto-invoked when working in apps\/app.",
    "user-invocable": false
}

App Specific Patterns (apps/app)

For general testing patterns, see the global .claude/skills/essential-test-patterns and .claude/skills/essential-test-design skills.

Next.js Pages Router

File Naming

Pages must use .page.tsx suffix:

pages/
├── _app.page.tsx           # App wrapper
├── [[...path]]/index.page.tsx  # Catch-all wiki pages
└── admin/index.page.tsx    # Admin pages

getLayout Pattern

// pages/admin/index.page.tsx
import type { NextPageWithLayout } from '~/interfaces/next-page';

const AdminPage: NextPageWithLayout = () => <AdminDashboard />;

AdminPage.getLayout = (page) => <AdminLayout>{page}</AdminLayout>;

export default AdminPage;

Jotai State Management

Directory Structure

src/states/
├── ui/
│   ├── sidebar/              # Multi-file feature
│   ├── device.ts             # Single-file feature
│   └── modal/                # 1 modal = 1 file
│       ├── page-create.ts
│       └── page-delete.ts
├── page/                     # Page data state
├── server-configurations/
└── context.ts

features/{name}/client/states/  # Feature-scoped atoms

Placement Rules

Category Location
UI state states/ui/
Modal state states/ui/modal/ (1 file per modal)
Page data states/page/
Feature-specific features/{name}/client/states/

Derived Atoms

import { atom } from 'jotai';

export const currentPageAtom = atom<Page | null>(null);

// Derived (read-only)
export const currentPagePathAtom = atom((get) => {
  return get(currentPageAtom)?.path ?? null;
});

SWR Data Fetching

Directory

src/stores-universal/
├── pages.ts       # Page hooks
├── users.ts       # User hooks
└── admin/settings.ts

Patterns

import useSWR from 'swr';
import useSWRImmutable from 'swr/immutable';

// Auto-revalidation
export const usePageList = () => useSWR<Page[]>('/api/v3/pages', fetcher);

// No auto-revalidation (static data)
export const usePageById = (id: string | null) =>
  useSWRImmutable<Page>(id ? `/api/v3/pages/${id}` : null, fetcher);

Testing (apps/app Specific)

Mocking Next.js Router

import { mockDeep } from 'vitest-mock-extended';
import type { NextRouter } from 'next/router';

const createMockRouter = (overrides = {}) => {
  const mock = mockDeep<NextRouter>();
  mock.pathname = '/test';
  mock.push.mockResolvedValue(true);
  return Object.assign(mock, overrides);
};

vi.mock('next/router', () => ({
  useRouter: () => createMockRouter(),
}));

Testing with Jotai

import { Provider } from 'jotai';
import { useHydrateAtoms } from 'jotai/utils';

const HydrateAtoms = ({ initialValues, children }) => {
  useHydrateAtoms(initialValues);
  return children;
};

const renderWithJotai = (ui, initialValues = []) => render(
  <Provider>
    <HydrateAtoms initialValues={initialValues}>{ui}</HydrateAtoms>
  </Provider>
);

// Usage
renderWithJotai(<PageHeader />, [[currentPageAtom, mockPage]]);

Testing SWR

import { SWRConfig } from 'swr';

const wrapper = ({ children }) => (
  <SWRConfig value={{ dedupingInterval: 0, provider: () => new Map() }}>
    {children}
  </SWRConfig>
);

const { result } = renderHook(() => usePageById('123'), { wrapper });

Path Aliases

Always use ~/ for imports:

import { PageService } from '~/server/services/PageService';
import { currentPageAtom } from '~/states/page/page-atoms';

Summary

  1. Next.js: .page.tsx suffix, getLayout for layouts
  2. Jotai: states/ global, features/*/client/states/ feature-scoped
  3. SWR: stores-universal/, null key for conditional fetch
  4. Testing: Mock router, hydrate Jotai, wrap SWR config
  5. Imports: Always ~/ path alias

版本历史

  • 0dc62d2 当前 2026-08-20 19:46

同 Skill 集合

.claude/skills/detect-flaky-ci/SKILL.md
.claude/skills/essential-test-design/SKILL.md
.claude/skills/essential-test-patterns/SKILL.md
.claude/skills/investigate-flaky-test/SKILL.md
.claude/skills/kiro-debug/SKILL.md
.claude/skills/kiro-discovery/SKILL.md
.claude/skills/kiro-impl/SKILL.md
.claude/skills/kiro-review/SKILL.md
.claude/skills/kiro-spec-batch/SKILL.md
.claude/skills/kiro-spec-cleanup/SKILL.md
.claude/skills/kiro-spec-design/SKILL.md
.claude/skills/kiro-spec-init/SKILL.md
.claude/skills/kiro-spec-quick/SKILL.md
.claude/skills/kiro-spec-requirements/SKILL.md
.claude/skills/kiro-spec-tasks/SKILL.md
.claude/skills/kiro-steering-custom/SKILL.md
.claude/skills/kiro-steering/SKILL.md
.claude/skills/kiro-validate-design/SKILL.md
.claude/skills/kiro-validate-gap/SKILL.md
.claude/skills/kiro-validate-impl/SKILL.md
.claude/skills/kiro-verify-completion/SKILL.md
.claude/skills/mongoose-to-prisma/SKILL.md
apps/app/.claude/skills/app-architecture/SKILL.md
apps/app/.claude/skills/app-commands/SKILL.md
apps/app/.claude/skills/build-optimization/SKILL.md
apps/app/.claude/skills/next-express-route-consistency/SKILL.md
apps/app/.claude/skills/vendor-styles-components/SKILL.md
.claude/skills/kiro-spec-status/SKILL.md
.claude/skills/suggest-path-evaluator/SKILL.md
apps/app/.claude/skills/esm-merge-coverage/SKILL.md

元信息

文件数
0
版本
b2fbe5b
Hash
a58f2441
收录时间
2026-08-20 19:46

首页 - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-09-21 16:50
浙ICP备14020137号-1