@lark-apaas/coding-templates 0.1.19 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-templates",
3
- "version": "0.1.19",
3
+ "version": "0.1.22",
4
4
  "description": "OpenClaw project templates for mclaw CLI",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,20 +23,20 @@
23
23
  │ ├── index.css # 全局样式 + 主题变量
24
24
  │ ├── api/ # API 请求封装
25
25
  │ │ └── index.ts
26
- │ ├── components/ # 全局共享组件
26
+ │ ├── components/ # 基础 UI 组件(禁止存放业务组件)
27
27
  │ │ └── ui/ # shadcn/ui 内置组件(勿手动修改)
28
28
  │ ├── pages/ # 页面模块(每个页面一个目录)
29
- │ │ ├── home/
30
- │ │ └── todos/ # CRUD 示例
31
- │ │ ├── index.tsx
32
- │ │ └── components/
29
+ │ │ ├── HomePage/ # 占位示例页,开发时替换为业务首页
30
+ │ │ └── HomePage.tsx # 页面入口文件与目录同名
31
+ │ │ │ └── components/ # 页面专属组件
32
+ │ │ └── NotFoundPage/
33
+ │ │ └── NotFoundPage.tsx
33
34
  │ ├── hooks/ # 自定义 Hooks
34
35
  │ └── lib/ # 工具函数(cn() 等)
35
36
  ├── server/ # 后端代码
36
37
  │ ├── index.ts # Express 入口(dev: Vite HMR 中间件)
37
38
  │ ├── routes/ # API 路由
38
- │ │ ├── index.ts # 路由注册
39
- │ │ └── todos.ts # Todos CRUD 路由
39
+ │ │ └── index.ts # 路由注册
40
40
  │ └── db/ # 数据库层
41
41
  │ ├── schema.ts # Drizzle schema 定义(可由工具生成)
42
42
  │ └── index.ts # 数据库连接
@@ -47,16 +47,6 @@
47
47
 
48
48
  ---
49
49
 
50
- ## 快速开始
51
-
52
- ```bash
53
- npm install
54
- cp _env.local.example .env.local # 配置 DATABASE_URL
55
- npm run dev # 启动开发服务器
56
- ```
57
-
58
- ---
59
-
60
50
  ## 新增资源(以 posts 为例)
61
51
 
62
52
  ### 1. shared/ — 定义类型和校验
@@ -103,7 +93,7 @@ export const posts = pgTable("posts", {
103
93
  import { Router } from "express";
104
94
  import { db } from "../db/index";
105
95
  import { posts } from "../db/schema";
106
- import { createPostSchema } from "../../shared/api.interface";
96
+ import { createPostSchema } from "@shared/api.interface";
107
97
 
108
98
  const router = Router();
109
99
  router.get("/", async (_req, res) => {
@@ -131,17 +121,22 @@ app.use("/api/posts", postsRouter);
131
121
  `client/src/api/index.ts` 增加封装:
132
122
 
133
123
  ```typescript
134
- import type { CreatePostRequest, CreatePostResponse, ListPostsResponse } from "@shared/api.interface";
135
-
136
- export const postsApi = {
137
- list: () => request<ListPostsResponse>("/api/posts"),
138
- create: (data: CreatePostRequest) => request<CreatePostResponse>("/api/posts", {
139
- method: "POST", body: JSON.stringify(data),
140
- }),
141
- };
124
+ import type { ListPostsResponse } from "@shared/api.interface";
125
+
126
+ export async function listPosts(): Promise<ListPostsResponse> {
127
+ try {
128
+ const response = await axiosForBackend({
129
+ url: '/api/posts',
130
+ method: 'GET',
131
+ });
132
+ return response.data;
133
+ } catch (error) {
134
+ throw error;
135
+ }
136
+ }
142
137
  ```
143
138
 
144
- `client/src/pages/posts/index.tsx` 编写页面,`client/src/app.tsx` 注册路由。
139
+ `client/src/pages/PostsPage/PostsPage.tsx` 编写页面,`client/src/app.tsx` 注册路由。
145
140
 
146
141
  ---
147
142
 
@@ -150,7 +145,7 @@ export const postsApi = {
150
145
  **页面文件只做骨架编排,不包含具体 UI 实现。**
151
146
 
152
147
  ```tsx
153
- // client/src/pages/dashboard/index.tsx
148
+ // client/src/pages/DashboardPage/DashboardPage.tsx
154
149
  import { StatsSection } from "./components/stats-section";
155
150
  import { DataTableSection } from "./components/data-table-section";
156
151
 
@@ -169,29 +164,57 @@ export default function DashboardPage() {
169
164
  - 每个视觉上独立的区块拆为一个组件文件,即使只出现一次
170
165
  - 单个组件文件不超过 **150 行**,超出时进一步拆分子组件
171
166
  - 页面专属组件放在 `pages/<page>/components/`
172
- - 跨页面复用的组件放在 `client/src/components/`
173
- - 相同 UI 片段出现 **≥2 次**时,必须提取为可复用组件
167
+ - `client/src/components/` 仅存放基础 UI 组件(如 shadcn/ui),**禁止存放业务组件**
174
168
  - 文件名 kebab-case(`stat-card.tsx`),组件名 PascalCase(`StatCard`)
175
169
  - 组件之间**禁止循环引用**
176
170
 
171
+ ### Section 独立性(并行开发规范)
172
+
173
+ 每个 Section 级组件(如 `StatsSection`、`DataTableSection`)必须做到**完全自包含**:
174
+
175
+ **1. 页面文件 = 纯布局组合器**
176
+
177
+ - 页面入口文件(如 `DashboardPage.tsx`)只做 import + JSX 排列,**禁止包含** `useState`、`useEffect`、数据请求或业务逻辑
178
+ - 页面文件无 props 接口定义,不承担任何数据协调职责
179
+
180
+ **2. Section 自包含原则**
181
+
182
+ 每个 Section 组件独立拥有自己的:
183
+ - **数据获取**(API 调用、fetch)— 即使多个 Section 需要同一份数据,各自获取
184
+ - **状态管理**(useState、useReducer)
185
+ - **类型定义**(写在同文件或同目录下的 `types.ts`)
186
+ - **子组件**(如需拆分,平铺在 `components/` 目录下,禁止嵌套子目录)
187
+
188
+ **3. 禁止 Section 间横向依赖**
189
+
190
+ - 兄弟 Section 之间**禁止互相 import**
191
+ - 兄弟 Section 之间**禁止通过 Context、全局 store、事件总线、页面 props 共享状态**
192
+ - 每个 Section 可独立开发、独立测试,不依赖其他 Section 的存在
193
+
177
194
  ---
178
195
 
179
196
  ## 路由注册
180
197
 
181
- 新增页面在 `client/src/app.tsx` 中注册:
198
+ 新增页面在 `client/src/app.tsx` 中注册。
199
+
200
+ > ⚠️ **首页路由替换(必做)**
201
+ >
202
+ > 模板默认的 `HomePage` 是占位示例页,**不是业务首页**。开发时必须将 `index` 路由替换为真实的业务首页组件(如 `DashboardPage`),并删除 `HomePage` 目录。
203
+
204
+ **替换后的路由示例:**
182
205
 
183
206
  ```tsx
184
207
  <Route element={<Layout />}>
185
- <Route index element={<HomePage />} />
208
+ {/* index 路由指向真实的业务首页 */}
209
+ <Route index element={<DashboardPage />} />
186
210
  <Route path="todos" element={<TodosPage />} />
187
- <Route path="dashboard" element={<DashboardPage />} />
188
211
  <Route path="*" element={<NotFoundPage />} />
189
212
  </Route>
190
213
  ```
191
214
 
192
215
  **新增页面步骤:**
193
216
 
194
- 1. 在 `client/src/pages/` 下新建页面目录和 `index.tsx`
217
+ 1. 在 `client/src/pages/` 下新建页面目录(如 `SettingsPage`)和 `SettingsPage.tsx`
195
218
  2. 在 `app.tsx` 的 `<Routes>` 内添加 `<Route>` 配置
196
219
 
197
220
  **路由跳转必须使用 react-router-dom:**
@@ -281,9 +304,9 @@ export default function DashboardPage() {
281
304
 
282
305
  | 检查项 | 验收标准 |
283
306
  |--------|---------|
284
- | 页面拆分 | 页面文件只做骨架编排;每个区块为独立组件;单文件 ≤150 行 |
285
- | 组件复用 | 相同片段 ≥2 次已提取为组件;文件名 kebab-case,组件名 PascalCase |
286
- | 路由注册 | 新页面已在 `app.tsx` 注册;跳转使用 `<Link>` / `useNavigate()`,无 `<a href>` |
307
+ | 页面拆分 | 页面文件只做骨架编排(无 state/effect/逻辑);每个 Section 自包含数据+状态+类型;兄弟 Section 间无互相 import;单文件 ≤150 行 |
308
+ | 命名规范 | 页面目录 PascalCase,页面入口文件与目录同名(PascalCase),组件文件名 kebab-case,组件名 PascalCase |
309
+ | 路由注册 | 默认 `HomePage` 已替换为业务首页;新页面已在 `app.tsx` 注册;跳转使用 `<Link>` / `useNavigate()`,无 `<a href>` |
287
310
  | API 调用 | 统一在 `api/` 封装;使用 `@shared` 类型;组件内不直接 fetch |
288
311
  | 输入校验 | zod schema 定义在 `shared/api.interface.ts`;server 和 client 共用 |
289
312
  | 主题色 | 使用语义化变量类(`bg-background`、`text-primary` 等);未硬编码颜色值 |
@@ -1,39 +1,17 @@
1
- import type {
2
- CreateTodoRequest,
3
- CreateTodoResponse,
4
- UpdateTodoRequest,
5
- UpdateTodoResponse,
6
- DeleteTodoResponse,
7
- ListTodosResponse,
8
- } from "@shared/api.interface";
1
+ import { axiosForBackend } from '@lark-apaas/client-toolkit-lite/utils/getAxiosForBackend';
2
+ // import type { ListPostsResponse } from '@shared/api.interface';
9
3
 
10
- async function request<T>(url: string, options?: RequestInit): Promise<T> {
11
- const res = await fetch(url, {
12
- headers: { "Content-Type": "application/json" },
13
- ...options,
14
- });
15
- if (!res.ok) {
16
- const body = await res.json().catch(() => ({}));
17
- throw new Error(body.error?.toString() ?? `Request failed: ${res.status}`);
18
- }
19
- return res.json();
20
- }
21
-
22
- export const todosApi = {
23
- list: () => request<ListTodosResponse>("/api/todos"),
24
-
25
- create: (data: CreateTodoRequest) =>
26
- request<CreateTodoResponse>("/api/todos", {
27
- method: "POST",
28
- body: JSON.stringify(data),
29
- }),
30
-
31
- update: (id: number, data: UpdateTodoRequest) =>
32
- request<UpdateTodoResponse>(`/api/todos/${id}`, {
33
- method: "PATCH",
34
- body: JSON.stringify(data),
35
- }),
36
-
37
- remove: (id: number) =>
38
- request<DeleteTodoResponse>(`/api/todos/${id}`, { method: "DELETE" }),
39
- };
4
+ // Add more API functions here, use axios instance (`axiosForBackend`) to make requests.
5
+ //
6
+ // 使用示例:
7
+ // export async function listPosts(): Promise<ListPostsResponse> {
8
+ // try {
9
+ // const response = await axiosForBackend({
10
+ // url: '/api/posts',
11
+ // method: 'GET',
12
+ // });
13
+ // return response.data;
14
+ // } catch (error) {
15
+ // throw error;
16
+ // }
17
+ // }
@@ -1,8 +1,7 @@
1
1
  import { BrowserRouter, Routes, Route } from "react-router-dom";
2
2
  import { Layout } from "@/components/layout";
3
- import HomePage from "@/pages/home";
4
- import TodosPage from "@/pages/todos";
5
- import NotFoundPage from "@/pages/not-found";
3
+ import HomePage from "@/pages/HomePage/HomePage";
4
+ import NotFoundPage from "@/pages/NotFoundPage/NotFoundPage";
6
5
 
7
6
  export default function App() {
8
7
  return (
@@ -10,7 +9,6 @@ export default function App() {
10
9
  <Routes>
11
10
  <Route element={<Layout />}>
12
11
  <Route index element={<HomePage />} />
13
- <Route path="todos" element={<TodosPage />} />
14
12
  <Route path="*" element={<NotFoundPage />} />
15
13
  </Route>
16
14
  </Routes>
@@ -2,10 +2,8 @@ import { Outlet } from "react-router-dom";
2
2
 
3
3
  export function Layout() {
4
4
  return (
5
- <div className="min-h-screen bg-background text-foreground">
6
- <main className="container mx-auto px-4 py-8">
7
- <Outlet />
8
- </main>
5
+ <div className="w-screen h-screen">
6
+ <Outlet />
9
7
  </div>
10
8
  );
11
9
  }
@@ -0,0 +1,12 @@
1
+ export default function HomePage() {
2
+ return (
3
+ <div className="flex flex-col items-center justify-center py-24">
4
+ <h1 className="text-5xl font-bold tracking-tight sm:text-7xl mb-4">
5
+ 👋 Hello OpenClaw
6
+ </h1>
7
+ <p className="text-lg text-muted-foreground">
8
+ Start building your app by editing <code className="px-1.5 py-0.5 rounded bg-muted font-mono text-sm">client/src/pages/HomePage/HomePage.tsx</code>
9
+ </p>
10
+ </div>
11
+ );
12
+ }
@@ -74,7 +74,7 @@
74
74
  "zod": "^4.3.6"
75
75
  },
76
76
  "devDependencies": {
77
- "@lark-apaas/coding-presets": "^0.1.0",
77
+ "@lark-apaas/coding-presets": "^0.2.0",
78
78
  "@lark-apaas/coding-vite-preset": "^0.1.0",
79
79
  "@types/express": "^5",
80
80
  "@types/node": "^24",
@@ -1,8 +1,11 @@
1
- import { pgTable, serial, text, boolean, timestamp } from "drizzle-orm/pg-core";
2
-
3
- export const todos = pgTable("todos", {
4
- id: serial("id").primaryKey(),
5
- title: text("title").notNull(),
6
- completed: boolean("completed").notNull().default(false),
7
- createdAt: timestamp("created_at").notNull().defaultNow(),
8
- });
1
+ // Drizzle schema 定义(可由工具生成)
2
+ //
3
+ // import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
4
+ //
5
+ // 使用示例:
6
+ // export const posts = pgTable("posts", {
7
+ // id: serial("id").primaryKey(),
8
+ // title: text("title").notNull(),
9
+ // content: text("content"),
10
+ // createdAt: timestamp("created_at").notNull().defaultNow(),
11
+ // });
@@ -1,6 +1,9 @@
1
1
  import type { Express } from "express";
2
- import todosRouter from "./todos";
2
+ // import postsRouter from "./posts";
3
3
 
4
4
  export function registerRoutes(app: Express) {
5
- app.use("/api/todos", todosRouter);
5
+ // 在此注册 API 路由
6
+ //
7
+ // 使用示例:
8
+ // app.use("/api/posts", postsRouter);
6
9
  }
@@ -1,23 +1,13 @@
1
1
  // API 接口契约定义
2
2
  // zod schema 做校验,z.infer 推导类型,前后端共享
3
-
4
- import { z } from "zod";
5
- import type { Todo } from "./types";
6
-
7
- // ----- Todos -----
8
-
9
- export const createTodoSchema = z.object({
10
- title: z.string().min(1).max(500),
11
- });
12
-
13
- export const updateTodoSchema = z.object({
14
- title: z.string().min(1).max(500).optional(),
15
- completed: z.boolean().optional(),
16
- });
17
-
18
- export type CreateTodoRequest = z.infer<typeof createTodoSchema>;
19
- export type UpdateTodoRequest = z.infer<typeof updateTodoSchema>;
20
- export type CreateTodoResponse = Todo;
21
- export type UpdateTodoResponse = Todo;
22
- export type DeleteTodoResponse = Todo;
23
- export type ListTodosResponse = Todo[];
3
+ //
4
+ // import { z } from "zod";
5
+ // import type { Post } from "./types";
6
+ //
7
+ // 使用示例:
8
+ // export const createPostSchema = z.object({
9
+ // title: z.string().min(1).max(200),
10
+ // content: z.string().optional(),
11
+ // });
12
+ // export type CreatePostRequest = z.infer<typeof createPostSchema>;
13
+ // export type CreatePostResponse = Post;
@@ -1,9 +1,10 @@
1
1
  // 前后端共享类型定义
2
2
  // shared 是最底层模块,不依赖 client 或 server
3
-
4
- export interface Todo {
5
- id: number;
6
- title: string;
7
- completed: boolean;
8
- createdAt: string;
9
- }
3
+ //
4
+ // 使用示例:
5
+ // export interface Post {
6
+ // id: number;
7
+ // title: string;
8
+ // content: string | null;
9
+ // createdAt: string;
10
+ // }
@@ -4,6 +4,8 @@
4
4
  "outDir": "./dist/server",
5
5
  "rootDir": ".",
6
6
  "noEmit": false,
7
+ "allowImportingTsExtensions": false,
8
+ "noUnusedParameters": false,
7
9
  "baseUrl": ".",
8
10
  "paths": {
9
11
  "@shared/*": ["./shared/*"]
@@ -1,20 +0,0 @@
1
- import { Link } from "react-router-dom";
2
-
3
- export default function HomePage() {
4
- return (
5
- <div className="flex flex-col items-center justify-center py-24">
6
- <h1 className="text-5xl font-bold tracking-tight sm:text-7xl mb-4">
7
- Hello OpenClaw
8
- </h1>
9
- <p className="text-lg text-muted-foreground mb-8">
10
- Apex full-stack template
11
- </p>
12
- <Link
13
- to="/todos"
14
- className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-sm font-medium text-primary-foreground hover:bg-primary/90"
15
- >
16
- Try the Todos Demo
17
- </Link>
18
- </div>
19
- );
20
- }
@@ -1,40 +0,0 @@
1
- import { useState } from "react";
2
-
3
- interface TodoFormProps {
4
- onSubmit: (title: string) => Promise<void>;
5
- }
6
-
7
- export function TodoForm({ onSubmit }: TodoFormProps) {
8
- const [title, setTitle] = useState("");
9
- const [submitting, setSubmitting] = useState(false);
10
-
11
- const handleSubmit = async (e: React.FormEvent) => {
12
- e.preventDefault();
13
- const trimmed = title.trim();
14
- if (!trimmed) return;
15
- setSubmitting(true);
16
- await onSubmit(trimmed);
17
- setTitle("");
18
- setSubmitting(false);
19
- };
20
-
21
- return (
22
- <form onSubmit={handleSubmit} className="flex gap-2">
23
- <input
24
- type="text"
25
- value={title}
26
- onChange={(e) => setTitle(e.target.value)}
27
- placeholder="What needs to be done?"
28
- className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
29
- disabled={submitting}
30
- />
31
- <button
32
- type="submit"
33
- disabled={submitting || !title.trim()}
34
- className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50"
35
- >
36
- Add
37
- </button>
38
- </form>
39
- );
40
- }
@@ -1,43 +0,0 @@
1
- import type { Todo } from "@shared/types";
2
- import { Trash2 } from "lucide-react";
3
-
4
- interface TodoListProps {
5
- todos: Todo[];
6
- onToggle: (id: number, completed: boolean) => Promise<void>;
7
- onDelete: (id: number) => Promise<void>;
8
- }
9
-
10
- export function TodoList({ todos, onToggle, onDelete }: TodoListProps) {
11
- if (todos.length === 0) {
12
- return <p className="text-muted-foreground mt-8 text-center">No todos yet.</p>;
13
- }
14
-
15
- return (
16
- <ul className="mt-6 space-y-2">
17
- {todos.map((todo) => (
18
- <li
19
- key={todo.id}
20
- className="flex items-center gap-3 rounded-md border border-border px-4 py-3"
21
- >
22
- <input
23
- type="checkbox"
24
- checked={todo.completed}
25
- onChange={() => onToggle(todo.id, !todo.completed)}
26
- className="h-4 w-4 rounded border-border"
27
- />
28
- <span
29
- className={`flex-1 text-sm ${todo.completed ? "line-through text-muted-foreground" : ""}`}
30
- >
31
- {todo.title}
32
- </span>
33
- <button
34
- onClick={() => onDelete(todo.id)}
35
- className="text-muted-foreground hover:text-destructive transition-colors"
36
- >
37
- <Trash2 className="h-4 w-4" />
38
- </button>
39
- </li>
40
- ))}
41
- </ul>
42
- );
43
- }
@@ -1,47 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import type { Todo } from "@shared/types";
3
- import { todosApi } from "@/api";
4
- import { TodoForm } from "./components/todo-form";
5
- import { TodoList } from "./components/todo-list";
6
-
7
- export default function TodosPage() {
8
- const [todos, setTodos] = useState<Todo[]>([]);
9
- const [loading, setLoading] = useState(true);
10
-
11
- const refresh = async () => {
12
- const list = await todosApi.list();
13
- setTodos(list);
14
- setLoading(false);
15
- };
16
-
17
- useEffect(() => {
18
- refresh();
19
- }, []);
20
-
21
- const handleCreate = async (title: string) => {
22
- await todosApi.create({ title });
23
- await refresh();
24
- };
25
-
26
- const handleToggle = async (id: number, completed: boolean) => {
27
- await todosApi.update(id, { completed });
28
- await refresh();
29
- };
30
-
31
- const handleDelete = async (id: number) => {
32
- await todosApi.remove(id);
33
- await refresh();
34
- };
35
-
36
- return (
37
- <div className="max-w-2xl mx-auto py-12">
38
- <h1 className="text-3xl font-bold mb-8">Todos</h1>
39
- <TodoForm onSubmit={handleCreate} />
40
- {loading ? (
41
- <p className="text-muted-foreground mt-8">Loading...</p>
42
- ) : (
43
- <TodoList todos={todos} onToggle={handleToggle} onDelete={handleDelete} />
44
- )}
45
- </div>
46
- );
47
- }
@@ -1,53 +0,0 @@
1
- import { Router } from "express";
2
- import { eq, desc } from "drizzle-orm";
3
- import { db } from "../db/index";
4
- import { todos } from "../db/schema";
5
- import { createTodoSchema, updateTodoSchema } from "../../shared/api.interface";
6
-
7
- const router = Router();
8
-
9
- // GET /api/todos
10
- router.get("/", async (_req, res) => {
11
- const list = await db.select().from(todos).orderBy(desc(todos.createdAt));
12
- res.json(list);
13
- });
14
-
15
- // POST /api/todos
16
- router.post("/", async (req, res) => {
17
- const parsed = createTodoSchema.safeParse(req.body);
18
- if (!parsed.success) {
19
- res.status(400).json({ error: parsed.error.flatten() });
20
- return;
21
- }
22
- const [todo] = await db.insert(todos).values({ title: parsed.data.title }).returning();
23
- res.status(201).json(todo);
24
- });
25
-
26
- // PATCH /api/todos/:id
27
- router.patch("/:id", async (req, res) => {
28
- const id = Number(req.params.id);
29
- const parsed = updateTodoSchema.safeParse(req.body);
30
- if (!parsed.success) {
31
- res.status(400).json({ error: parsed.error.flatten() });
32
- return;
33
- }
34
- const [todo] = await db.update(todos).set(parsed.data).where(eq(todos.id, id)).returning();
35
- if (!todo) {
36
- res.status(404).json({ error: "Not found" });
37
- return;
38
- }
39
- res.json(todo);
40
- });
41
-
42
- // DELETE /api/todos/:id
43
- router.delete("/:id", async (req, res) => {
44
- const id = Number(req.params.id);
45
- const [todo] = await db.delete(todos).where(eq(todos.id, id)).returning();
46
- if (!todo) {
47
- res.status(404).json({ error: "Not found" });
48
- return;
49
- }
50
- res.json(todo);
51
- });
52
-
53
- export default router;