@farris/cli 2.3.12 → 2.3.14

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.
Files changed (52) hide show
  1. package/lib/commands/add-project.js +3 -0
  2. package/lib/commands/add-project.js.map +1 -1
  3. package/lib/commands/create.js +8 -1
  4. package/lib/commands/create.js.map +1 -1
  5. package/lib/common/constant.js +4 -1
  6. package/lib/common/constant.js.map +1 -1
  7. package/lib/common/generate-app.js +5 -0
  8. package/lib/common/generate-app.js.map +1 -1
  9. package/lib/common/registry-notice.js +38 -0
  10. package/lib/common/registry-notice.js.map +1 -0
  11. package/lib/config/vite-app.js +5 -1
  12. package/lib/config/vite-app.js.map +1 -1
  13. package/lib/plugins/html-classic.js +24 -0
  14. package/lib/plugins/html-classic.js.map +1 -0
  15. package/lib/plugins/mock.js +12 -2
  16. package/lib/plugins/mock.js.map +1 -1
  17. package/package.json +93 -93
  18. package/templates/create/AGENTS.md +16 -1
  19. package/templates/create/README.md +4 -0
  20. package/templates/lib/package.json +1 -0
  21. package/templates/lib/tsconfig.json +4 -2
  22. package/templates/lib/vitest.config.ts +4 -1
  23. package/templates/lib/vitest.setup.ts +29 -0
  24. package/templates/micro/mock/README.md +7 -2
  25. package/templates/micro/mock/user.mock.ts +24 -0
  26. package/templates/micro/package.json +1 -1
  27. package/templates/micro/src/api/http-client.ts +38 -0
  28. package/templates/micro/src/api/index.ts +1 -0
  29. package/templates/micro/src/api/user.api.ts +21 -0
  30. package/templates/micro/src/views/HomeView.tsx +33 -1
  31. package/templates/micro/src/views/__tests__/HomeView.spec.tsx +2 -0
  32. package/templates/micro/tsconfig.json +3 -1
  33. package/templates/micro/vitest.config.ts +4 -1
  34. package/templates/micro/vitest.setup.ts +29 -0
  35. package/templates/mobile/mock/README.md +7 -2
  36. package/templates/mobile/tsconfig.json +3 -1
  37. package/templates/mobile/vitest.config.ts +4 -1
  38. package/templates/mobile/vitest.setup.ts +29 -0
  39. package/templates/web/mock/README.md +7 -2
  40. package/templates/web/mock/user.mock.ts +24 -0
  41. package/templates/web/package.json +1 -1
  42. package/templates/web/src/api/http-client.ts +38 -0
  43. package/templates/web/src/api/index.ts +1 -0
  44. package/templates/web/src/api/user.api.ts +21 -0
  45. package/templates/web/src/views/HomeView.tsx +33 -1
  46. package/templates/web/src/views/__tests__/HomeView.spec.tsx +2 -0
  47. package/templates/web/tsconfig.json +3 -1
  48. package/templates/web/vitest.config.ts +4 -1
  49. package/templates/web/vitest.setup.ts +29 -0
  50. package/templates/micro/mock/tsconfig.json +0 -10
  51. package/templates/mobile/mock/tsconfig.json +0 -10
  52. package/templates/web/mock/tsconfig.json +0 -10
@@ -56,6 +56,7 @@
56
56
  - 联调真实后端:在**工程目录内**执行 `pnpm dev --no-mock`(或 `MOCK=none pnpm dev`),业务代码不改。
57
57
  - 请求路径必须与 mock 的 `url` 逐字一致;禁止在组件里散落假数据。
58
58
  - mock 不得进入构建产物:`msw` 已在应用 `devDependencies`(开发期依赖),不要在 `mock/` 之外引用它,也不要把它加到 `dependencies`。
59
+ - mock 目录里的用例放 `mock/__tests__/`:`*.spec.*` / `*.test.*` / `*.d.ts` / `_` 前缀的文件与目录都不会被当作 handler 加载。mock 目录在工程 `tsconfig.json` 的 `include` 内,`pnpm typecheck` 会一并检查。
59
60
 
60
61
  ### 2.6 工程内生成器
61
62
 
@@ -97,8 +98,22 @@
97
98
 
98
99
  - 文件名 = 被测文件名 + `.spec`:纯逻辑用 `*.spec.ts`,组件或含 JSX 用 `*.spec.tsx`。
99
100
  - `__tests__/` 与被测文件同级;**不要**在工程根另建 `test/` 汇总目录。
101
+ - mock 数据的用例同样就近,放 `mock/__tests__/<模块>.spec.ts`:加载器按语义跳过它,不需要靠 `_` 前缀躲开。
100
102
  - 工程里**不预置**端到端测试:需要时自行引入,别与上面的单元测试混放。
101
103
 
104
+ **测试环境**:jsdom 没有实现全部浏览器 API(如 `ResizeObserver`)。缺口统一在工程根 `vitest.setup.ts` 里补齐,
105
+ 由 `vitest.config.ts` 的 `setupFiles` 引入;用例里不要再各写一份,需要新 API 就往那个文件加。
106
+
107
+ **测用到 Farris 组件的视图**:组件在 `src/main.ts` 里是全局注册的,测试挂载时要自己带上插件,否则模板里的 `<f-xxx>` 渲染不出来:
108
+
109
+ ```tsx
110
+ import FarrisVue from '@farris/ui-vue';
111
+ import { mount } from '@vue/test-utils';
112
+ import ProjectView from '../ProjectView';
113
+
114
+ const wrapper = mount(ProjectView, { global: { plugins: [FarrisVue] } });
115
+ ```
116
+
102
117
  ### 4.2 每次改动必须做
103
118
 
104
119
  1. 改完代码先在**工程目录内**跑 `pnpm test`;提交前在根目录跑一次 `pnpm test`(递归全部工程)。
@@ -108,7 +123,7 @@
108
123
 
109
124
  ### 4.3 禁止
110
125
 
111
- - 禁止为让测试通过而改 `vitest.config.ts` 的 `include` / `exclude` / `environment`,或删改既有用例。
126
+ - 禁止为让测试通过而改 `vitest.config.ts` 的 `include` / `exclude` / `environment`,或删改既有用例。缺的 jsdom API 加到 `vitest.setup.ts`,不要在每个用例里自己打补丁。
112
127
  - 禁止把端到端用例放进 `__tests__/`;禁止提交 `.only` / `.skip`。
113
128
  - 禁止在用例里靠固定 `sleep` 等待,或依赖上一条用例残留的状态。
114
129
 
@@ -13,6 +13,8 @@ pnpm test # 跑全部工程的单元测试
13
13
  pnpm verify # 校验 workspace 规范(改结构 / 脚本 / 容器后必跑)
14
14
  ```
15
15
 
16
+ **装依赖之前先看一眼 `.npmrc`**:它按组织内网源写好了 `registry=`。不在该网络内(外网、客户现场、其他团队)请先改掉再 `pnpm install`,否则会直接报错;两种改法任选:编辑根目录 `.npmrc` 里的这一行,或执行 `pnpm config set registry <你的源>`。
17
+
16
18
  开发某个工程时,进入它的目录:
17
19
 
18
20
  ```bash
@@ -20,6 +22,8 @@ cd <%= appContainer %>/shop # 以应用为例:工程内的命令都在这
20
22
  pnpm dev # 启动开发服务器(根目录没有 dev,是有意为之)
21
23
  ```
22
24
 
25
+ 首页默认会请求 `/api/v1/users`,数据来自工程内 `mock/user.mock.ts`(假接口,写法见 `mock/README.md`):打开页面就能看到列表,`pnpm dev --no-mock` 则会转去请求真实后端。
26
+
23
27
  ## 目录结构
24
28
 
25
29
  ```text
@@ -50,6 +50,7 @@
50
50
  "@vue/eslint-config-typescript": "^13.0.0",
51
51
  "@vue/test-utils": "^2.4.6",
52
52
  "@vue/tsconfig": "^0.5.1",
53
+ "esbuild-sass-plugin": "^3.3.1",
53
54
  "eslint": "^8.57.0",
54
55
  "eslint-plugin-vue": "^9.23.0",
55
56
  "jsdom": "^25.0.1",
@@ -2,9 +2,11 @@
2
2
  "extends": "@vue/tsconfig/tsconfig.dom.json",
3
3
  "include": [
4
4
  "components/**/*",
5
- "src/**/*"
5
+ "src/**/*",
6
+ "vitest.setup.ts"
6
7
  ],
7
8
  "compilerOptions": {
8
- "strict": true
9
+ "strict": true,
10
+ "types": ["vite/client"]
9
11
  }
10
12
  }
@@ -1,10 +1,13 @@
1
1
  import vue from '@vitejs/plugin-vue';
2
+ import vueJsx from '@vitejs/plugin-vue-jsx';
2
3
  import { configDefaults, defineConfig } from 'vitest/config';
3
4
 
4
5
  export default defineConfig({
5
- plugins: [vue()],
6
+ plugins: [vue(), vueJsx()],
6
7
  test: {
7
8
  environment: 'jsdom',
9
+ // jsdom 缺失的浏览器 API(如 ResizeObserver)统一在这里补齐,用例里不要再各写一份
10
+ setupFiles: ['./vitest.setup.ts'],
8
11
  // 官方默认收集范围(**/*.{test,spec}.*),源码旁的测试才会被收集
9
12
  include: [...configDefaults.include],
10
13
  exclude: [...configDefaults.exclude, '**/__sandbox_*/**']
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 测试运行前的环境兜底(由 `vitest.config.ts` 的 `setupFiles` 引入)。
3
+ *
4
+ * jsdom 没有实现全部浏览器 API,而组件在挂载时会直接读它们:例如依赖尺寸观测的组件,
5
+ * 缺 `ResizeObserver` 就报错。统一在这里补齐,不要在用例里各写一份。
6
+ * 需要更多 API(`IntersectionObserver`、`matchMedia` 等)时同样是往这里加。
7
+ */
8
+ class ResizeObserverStub implements ResizeObserver {
9
+ constructor(private readonly callback: ResizeObserverCallback) {}
10
+
11
+ /**
12
+ * 同步回调一次。只写空方法的话,靠回调算尺寸的组件在用例里永远不会被触发,
13
+ * 测试就变成"挂上去就完事"的假绿。
14
+ */
15
+ observe(target: Element): void {
16
+ const entry = [
17
+ { target, contentRect: (target as HTMLElement).getBoundingClientRect() }
18
+ ] as unknown as ResizeObserverEntry[];
19
+ this.callback(entry, this);
20
+ }
21
+
22
+ unobserve(): void {}
23
+
24
+ disconnect(): void {}
25
+ }
26
+
27
+ if (typeof globalThis.ResizeObserver === 'undefined') {
28
+ globalThis.ResizeObserver = ResizeObserverStub;
29
+ }
@@ -26,6 +26,11 @@ export const handlers = [
26
26
  3. 在工程目录执行 `pnpm dev`,打开用到该接口的页面,**验证**返回的是 mock 数据。
27
27
  4. 需要联调真实后端时执行 `pnpm dev --no-mock`(或 `MOCK=none pnpm dev`),业务代码不改。
28
28
 
29
+ ## 给 mock 数据写测试
30
+
31
+ mock 目录里若有纯数据操作(种子数据、查询、增删改),值得单测:落点与工程其他用例一致,放 `mock/__tests__/<模块>.spec.ts`。
32
+ 用例会被 `pnpm test` 收集、被 `pnpm typecheck` 检查,也**不会**被当成 handler 加载(见下面规则),不需要靠 `_` 前缀躲开加载器。
33
+
29
34
  规则:
30
35
 
31
36
  - url 必须与代码实际发出的请求路径逐字一致,前缀也要带上;请求路径见 `src/api/` 下的接口模块。
@@ -34,7 +39,7 @@ export const handlers = [
34
39
  - 同一前缀下同时有字面量路径与 `:参数名` 路径时,字面量写在前面。MSW 取第一个匹配的 handler,`/api/v1/users/current` 排在 `/api/v1/users/:id` 后面会被 `:id` 接走。
35
40
  - 响应体必须能被工程的 HTTP 客户端正确消费,形状跟着客户端出口走。数据要有业务语义,用中文姓名、机构、单据号这类内容,不要用占位文本。
36
41
  - 关键场景要模拟:空态、错误、慢响应。错误用 `HttpResponse.json(..., { status: 500 })`;慢响应用 `delay`。
37
- - 目录里只放 handler 文件。共享数据或工具函数要放进 `_` 开头的文件,例如 `_data.ts`,加载时会跳过;其他文件会被当作 handler 文件加载,导出内容不是 handler 就会报错。
42
+ - 目录里只放 handler 文件。共享数据或工具函数放 `_` 开头的文件(如 `_data.ts`),用例放 `mock/__tests__/`。`_` 前缀的文件与目录、`__tests__/` 目录、`*.spec.*` / `*.test.*` / `*.d.ts` 都不会被加载;其余文件会被当作 handler 文件加载,导出内容不是 handler 就会报错。
38
43
  - 同一路径不允许在多个 mock 文件中重复定义,重复时开发服务启动或请求即报错。
39
44
  - 文件只导出 handler 数组,写 `export const handlers = [...]` 或 `export default [...]`。不需要 `setupServer` / `setupWorker` 接线;接线由脚手架完成。
40
45
  - `msw` 已由模板放进应用 `devDependencies`,版本随工作区 catalog 统一。不要把它加到 `dependencies`,也不要在 `mock/` 之外引用它,否则构建期会报错。
@@ -48,6 +53,6 @@ export const handlers = [
48
53
 
49
54
  开关:在工程目录执行 `pnpm dev` 默认生效;`pnpm dev --no-mock` 或 `MOCK=none pnpm dev` 进入真实后端联调。
50
55
 
51
- 类型:本目录附带独立的 `tsconfig.json`,只服务于编辑器与按需类型检查。应用自身的 `typecheck` 不包含本目录,mock 也不参与构建。
56
+ 类型:本目录在工程 `tsconfig.json` 的 `include` 内,`pnpm typecheck` 会一并检查(含 `mock/__tests__/` 下的用例)。mock 不参与构建。
52
57
 
53
58
  约定版本 2026-09。本文件随 `@farris/cli` 模板发布,AI 协作以本文件为准。
@@ -0,0 +1,24 @@
1
+ import { http, HttpResponse, delay } from 'msw';
2
+
3
+ /**
4
+ * 用户接口的假数据(开发期)。
5
+ *
6
+ * - 路径必须与 src/api/user.api.ts 里实际发出的请求**逐字一致**(前缀也要带上);
7
+ * - 只写路径,不要写协议与主机;
8
+ * - 存成 mock/*.mock.ts 保存即生效,不需要重启 pnpm dev;
9
+ * - 联调真实后端:pnpm dev --no-mock(或 MOCK=none pnpm dev),业务代码不用改。
10
+ *
11
+ * 完整规则见同目录 README.md,约定版本 2026-09。
12
+ */
13
+ export const handlers = [
14
+ http.get('/api/v1/users', async () => {
15
+ await delay(200);
16
+ return HttpResponse.json({
17
+ data: [
18
+ { id: 'u-1', name: '张三', dept: '财务部' },
19
+ { id: 'u-2', name: '李四', dept: '供应链部' }
20
+ ],
21
+ total: 2
22
+ });
23
+ })
24
+ ];
@@ -19,7 +19,7 @@
19
19
  "i18n": "farris-cli extract-i18n"
20
20
  },
21
21
  "dependencies": {
22
- "@farris/ui-vue": "latest",
22
+ "@farris/ui-vue": "<%= uiVueVersion %>",
23
23
  "vue": "^3.4.29",
24
24
  "vue-router": "^4.3.0"
25
25
  },
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Farris 标准 HTTP 请求客户端封装
3
+ */
4
+ export interface RequestOptions extends RequestInit {
5
+ params?: Record<string, any>;
6
+ }
7
+
8
+ export async function request<T = any>(url: string, options: RequestOptions = {}): Promise<T> {
9
+ const { params, ...customConfig } = options;
10
+
11
+ let requestUrl = url;
12
+ if (params && Object.keys(params).length > 0) {
13
+ const searchParams = new URLSearchParams();
14
+ Object.entries(params).forEach(([key, val]) => {
15
+ if (val !== undefined && val !== null) {
16
+ searchParams.append(key, String(val));
17
+ }
18
+ });
19
+ requestUrl += (url.includes('?') ? '&' : '?') + searchParams.toString();
20
+ }
21
+
22
+ const headers = {
23
+ 'Content-Type': 'application/json',
24
+ ...(customConfig.headers || {})
25
+ };
26
+
27
+ const response = await fetch(requestUrl, {
28
+ ...customConfig,
29
+ headers
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const errorText = await response.text().catch(() => response.statusText);
34
+ throw new Error(`HTTP Error ${response.status}: ${errorText}`);
35
+ }
36
+
37
+ return response.json();
38
+ }
@@ -0,0 +1 @@
1
+ export * from './user.api';
@@ -0,0 +1,21 @@
1
+ import { request } from './http-client';
2
+
3
+ export interface UserItem {
4
+ id: string;
5
+ name: string;
6
+ dept: string;
7
+ }
8
+
9
+ export interface UserListResponse {
10
+ data: UserItem[];
11
+ total?: number;
12
+ }
13
+
14
+ /**
15
+ * 用户列表。
16
+ *
17
+ * 开发期由工程内 mock/user.mock.ts 提供假数据;联调真实后端执行 pnpm dev --no-mock。
18
+ */
19
+ export async function getUserList(): Promise<UserListResponse> {
20
+ return request<UserListResponse>('/api/v1/users');
21
+ }
@@ -1,11 +1,43 @@
1
- import { defineComponent } from 'vue';
1
+ import { defineComponent, onMounted, ref } from 'vue';
2
+ import { getUserList, type UserItem } from '../api/user.api';
2
3
 
3
4
  export default defineComponent({
4
5
  name: 'HomeView',
5
6
  setup() {
7
+ const users = ref<UserItem[]>([]);
8
+ const loading = ref(false);
9
+ const error = ref('');
10
+
11
+ async function loadUsers(): Promise<void> {
12
+ loading.value = true;
13
+ error.value = '';
14
+ try {
15
+ users.value = (await getUserList()).data;
16
+ } catch (err) {
17
+ // 后端不可用时不阻塞页面:把失败原因显示出来即可
18
+ error.value = err instanceof Error ? err.message : '请求失败';
19
+ } finally {
20
+ loading.value = false;
21
+ }
22
+ }
23
+
24
+ onMounted(loadUsers);
25
+
6
26
  return () => (
7
27
  <main>
8
28
  <h1>Home</h1>
29
+ <p>下面的数据来自工程内 mock/ 目录的假接口;联调真实后端执行 pnpm dev --no-mock。</p>
30
+ {loading.value ? <p>加载中…</p> : null}
31
+ {!loading.value && error.value ? <p>加载失败:{error.value}</p> : null}
32
+ {!loading.value && !error.value ? (
33
+ <ul>
34
+ {users.value.map((user) => (
35
+ <li key={user.id}>
36
+ {user.name}({user.dept})
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ ) : null}
9
41
  <p>在 src/views/ 下新增页面;通用组件按需创建在 src/components/ 下。</p>
10
42
  </main>
11
43
  );
@@ -4,8 +4,10 @@ import HomeView from '../HomeView';
4
4
 
5
5
  describe('HomeView', () => {
6
6
  it('mounts', () => {
7
+ // 页面挂载时会请求接口(开发期由 mock/ 提供数据),这里只断言首屏能渲染
7
8
  const wrapper = mount(HomeView);
8
9
 
9
10
  expect(wrapper.exists()).toBe(true);
11
+ expect(wrapper.text()).toContain('Home');
10
12
  });
11
13
  });
@@ -3,7 +3,9 @@
3
3
  "include": [
4
4
  "env.d.ts",
5
5
  "src/**/*",
6
- "src/**/*.vue"
6
+ "src/**/*.vue",
7
+ "mock/**/*",
8
+ "vitest.setup.ts"
7
9
  ],
8
10
  "compilerOptions": {
9
11
  "composite": true,
@@ -1,10 +1,13 @@
1
1
  import vue from '@vitejs/plugin-vue';
2
+ import vueJsx from '@vitejs/plugin-vue-jsx';
2
3
  import { configDefaults, defineConfig } from 'vitest/config';
3
4
 
4
5
  export default defineConfig({
5
- plugins: [vue()],
6
+ plugins: [vue(), vueJsx()],
6
7
  test: {
7
8
  environment: 'jsdom',
9
+ // jsdom 缺失的浏览器 API(如 ResizeObserver)统一在这里补齐,用例里不要再各写一份
10
+ setupFiles: ['./vitest.setup.ts'],
8
11
  // 官方默认收集范围(**/*.{test,spec}.*),源码旁的测试才会被收集
9
12
  include: [...configDefaults.include],
10
13
  exclude: [...configDefaults.exclude, '**/__sandbox_*/**']
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 测试运行前的环境兜底(由 `vitest.config.ts` 的 `setupFiles` 引入)。
3
+ *
4
+ * jsdom 没有实现全部浏览器 API,而组件在挂载时会直接读它们:例如依赖尺寸观测的组件,
5
+ * 缺 `ResizeObserver` 就报错。统一在这里补齐,不要在用例里各写一份。
6
+ * 需要更多 API(`IntersectionObserver`、`matchMedia` 等)时同样是往这里加。
7
+ */
8
+ class ResizeObserverStub implements ResizeObserver {
9
+ constructor(private readonly callback: ResizeObserverCallback) {}
10
+
11
+ /**
12
+ * 同步回调一次。只写空方法的话,靠回调算尺寸的组件在用例里永远不会被触发,
13
+ * 测试就变成"挂上去就完事"的假绿。
14
+ */
15
+ observe(target: Element): void {
16
+ const entry = [
17
+ { target, contentRect: (target as HTMLElement).getBoundingClientRect() }
18
+ ] as unknown as ResizeObserverEntry[];
19
+ this.callback(entry, this);
20
+ }
21
+
22
+ unobserve(): void {}
23
+
24
+ disconnect(): void {}
25
+ }
26
+
27
+ if (typeof globalThis.ResizeObserver === 'undefined') {
28
+ globalThis.ResizeObserver = ResizeObserverStub;
29
+ }
@@ -26,6 +26,11 @@ export const handlers = [
26
26
  3. 在工程目录执行 `pnpm dev`,打开用到该接口的页面,**验证**返回的是 mock 数据。
27
27
  4. 需要联调真实后端时执行 `pnpm dev --no-mock`(或 `MOCK=none pnpm dev`),业务代码不改。
28
28
 
29
+ ## 给 mock 数据写测试
30
+
31
+ mock 目录里若有纯数据操作(种子数据、查询、增删改),值得单测:落点与工程其他用例一致,放 `mock/__tests__/<模块>.spec.ts`。
32
+ 用例会被 `pnpm test` 收集、被 `pnpm typecheck` 检查,也**不会**被当成 handler 加载(见下面规则),不需要靠 `_` 前缀躲开加载器。
33
+
29
34
  规则:
30
35
 
31
36
  - url 必须与代码实际发出的请求路径逐字一致,前缀也要带上;请求路径见 `src/api/` 下的接口模块。
@@ -34,7 +39,7 @@ export const handlers = [
34
39
  - 同一前缀下同时有字面量路径与 `:参数名` 路径时,字面量写在前面。MSW 取第一个匹配的 handler,`/api/v1/users/current` 排在 `/api/v1/users/:id` 后面会被 `:id` 接走。
35
40
  - 响应体必须能被工程的 HTTP 客户端正确消费,形状跟着客户端出口走。数据要有业务语义,用中文姓名、机构、单据号这类内容,不要用占位文本。
36
41
  - 关键场景要模拟:空态、错误、慢响应。错误用 `HttpResponse.json(..., { status: 500 })`;慢响应用 `delay`。
37
- - 目录里只放 handler 文件。共享数据或工具函数要放进 `_` 开头的文件,例如 `_data.ts`,加载时会跳过;其他文件会被当作 handler 文件加载,导出内容不是 handler 就会报错。
42
+ - 目录里只放 handler 文件。共享数据或工具函数放 `_` 开头的文件(如 `_data.ts`),用例放 `mock/__tests__/`。`_` 前缀的文件与目录、`__tests__/` 目录、`*.spec.*` / `*.test.*` / `*.d.ts` 都不会被加载;其余文件会被当作 handler 文件加载,导出内容不是 handler 就会报错。
38
43
  - 同一路径不允许在多个 mock 文件中重复定义,重复时开发服务启动或请求即报错。
39
44
  - 文件只导出 handler 数组,写 `export const handlers = [...]` 或 `export default [...]`。不需要 `setupServer` / `setupWorker` 接线;接线由脚手架完成。
40
45
  - `msw` 已由模板放进应用 `devDependencies`,版本随工作区 catalog 统一。不要把它加到 `dependencies`,也不要在 `mock/` 之外引用它,否则构建期会报错。
@@ -48,6 +53,6 @@ export const handlers = [
48
53
 
49
54
  开关:在工程目录执行 `pnpm dev` 默认生效;`pnpm dev --no-mock` 或 `MOCK=none pnpm dev` 进入真实后端联调。
50
55
 
51
- 类型:本目录附带独立的 `tsconfig.json`,只服务于编辑器与按需类型检查。应用自身的 `typecheck` 不包含本目录,mock 也不参与构建。
56
+ 类型:本目录在工程 `tsconfig.json` 的 `include` 内,`pnpm typecheck` 会一并检查(含 `mock/__tests__/` 下的用例)。mock 不参与构建。
52
57
 
53
58
  约定版本 2026-09。本文件随 `@farris/cli` 模板发布,AI 协作以本文件为准。
@@ -3,7 +3,9 @@
3
3
  "include": [
4
4
  "env.d.ts",
5
5
  "src/**/*",
6
- "src/**/*.vue"
6
+ "src/**/*.vue",
7
+ "mock/**/*",
8
+ "vitest.setup.ts"
7
9
  ],
8
10
  "compilerOptions": {
9
11
  "composite": true,
@@ -1,10 +1,13 @@
1
1
  import vue from '@vitejs/plugin-vue';
2
+ import vueJsx from '@vitejs/plugin-vue-jsx';
2
3
  import { configDefaults, defineConfig } from 'vitest/config';
3
4
 
4
5
  export default defineConfig({
5
- plugins: [vue()],
6
+ plugins: [vue(), vueJsx()],
6
7
  test: {
7
8
  environment: 'jsdom',
9
+ // jsdom 缺失的浏览器 API(如 ResizeObserver)统一在这里补齐,用例里不要再各写一份
10
+ setupFiles: ['./vitest.setup.ts'],
8
11
  // 官方默认收集范围(**/*.{test,spec}.*),源码旁的测试才会被收集
9
12
  include: [...configDefaults.include],
10
13
  exclude: [...configDefaults.exclude, '**/__sandbox_*/**']
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 测试运行前的环境兜底(由 `vitest.config.ts` 的 `setupFiles` 引入)。
3
+ *
4
+ * jsdom 没有实现全部浏览器 API,而组件在挂载时会直接读它们:例如依赖尺寸观测的组件,
5
+ * 缺 `ResizeObserver` 就报错。统一在这里补齐,不要在用例里各写一份。
6
+ * 需要更多 API(`IntersectionObserver`、`matchMedia` 等)时同样是往这里加。
7
+ */
8
+ class ResizeObserverStub implements ResizeObserver {
9
+ constructor(private readonly callback: ResizeObserverCallback) {}
10
+
11
+ /**
12
+ * 同步回调一次。只写空方法的话,靠回调算尺寸的组件在用例里永远不会被触发,
13
+ * 测试就变成"挂上去就完事"的假绿。
14
+ */
15
+ observe(target: Element): void {
16
+ const entry = [
17
+ { target, contentRect: (target as HTMLElement).getBoundingClientRect() }
18
+ ] as unknown as ResizeObserverEntry[];
19
+ this.callback(entry, this);
20
+ }
21
+
22
+ unobserve(): void {}
23
+
24
+ disconnect(): void {}
25
+ }
26
+
27
+ if (typeof globalThis.ResizeObserver === 'undefined') {
28
+ globalThis.ResizeObserver = ResizeObserverStub;
29
+ }
@@ -26,6 +26,11 @@ export const handlers = [
26
26
  3. 在工程目录执行 `pnpm dev`,打开用到该接口的页面,**验证**返回的是 mock 数据。
27
27
  4. 需要联调真实后端时执行 `pnpm dev --no-mock`(或 `MOCK=none pnpm dev`),业务代码不改。
28
28
 
29
+ ## 给 mock 数据写测试
30
+
31
+ mock 目录里若有纯数据操作(种子数据、查询、增删改),值得单测:落点与工程其他用例一致,放 `mock/__tests__/<模块>.spec.ts`。
32
+ 用例会被 `pnpm test` 收集、被 `pnpm typecheck` 检查,也**不会**被当成 handler 加载(见下面规则),不需要靠 `_` 前缀躲开加载器。
33
+
29
34
  规则:
30
35
 
31
36
  - url 必须与代码实际发出的请求路径逐字一致,前缀也要带上;请求路径见 `src/api/` 下的接口模块。
@@ -34,7 +39,7 @@ export const handlers = [
34
39
  - 同一前缀下同时有字面量路径与 `:参数名` 路径时,字面量写在前面。MSW 取第一个匹配的 handler,`/api/v1/users/current` 排在 `/api/v1/users/:id` 后面会被 `:id` 接走。
35
40
  - 响应体必须能被工程的 HTTP 客户端正确消费,形状跟着客户端出口走。数据要有业务语义,用中文姓名、机构、单据号这类内容,不要用占位文本。
36
41
  - 关键场景要模拟:空态、错误、慢响应。错误用 `HttpResponse.json(..., { status: 500 })`;慢响应用 `delay`。
37
- - 目录里只放 handler 文件。共享数据或工具函数要放进 `_` 开头的文件,例如 `_data.ts`,加载时会跳过;其他文件会被当作 handler 文件加载,导出内容不是 handler 就会报错。
42
+ - 目录里只放 handler 文件。共享数据或工具函数放 `_` 开头的文件(如 `_data.ts`),用例放 `mock/__tests__/`。`_` 前缀的文件与目录、`__tests__/` 目录、`*.spec.*` / `*.test.*` / `*.d.ts` 都不会被加载;其余文件会被当作 handler 文件加载,导出内容不是 handler 就会报错。
38
43
  - 同一路径不允许在多个 mock 文件中重复定义,重复时开发服务启动或请求即报错。
39
44
  - 文件只导出 handler 数组,写 `export const handlers = [...]` 或 `export default [...]`。不需要 `setupServer` / `setupWorker` 接线;接线由脚手架完成。
40
45
  - `msw` 已由模板放进应用 `devDependencies`,版本随工作区 catalog 统一。不要把它加到 `dependencies`,也不要在 `mock/` 之外引用它,否则构建期会报错。
@@ -48,6 +53,6 @@ export const handlers = [
48
53
 
49
54
  开关:在工程目录执行 `pnpm dev` 默认生效;`pnpm dev --no-mock` 或 `MOCK=none pnpm dev` 进入真实后端联调。
50
55
 
51
- 类型:本目录附带独立的 `tsconfig.json`,只服务于编辑器与按需类型检查。应用自身的 `typecheck` 不包含本目录,mock 也不参与构建。
56
+ 类型:本目录在工程 `tsconfig.json` 的 `include` 内,`pnpm typecheck` 会一并检查(含 `mock/__tests__/` 下的用例)。mock 不参与构建。
52
57
 
53
58
  约定版本 2026-09。本文件随 `@farris/cli` 模板发布,AI 协作以本文件为准。
@@ -0,0 +1,24 @@
1
+ import { http, HttpResponse, delay } from 'msw';
2
+
3
+ /**
4
+ * 用户接口的假数据(开发期)。
5
+ *
6
+ * - 路径必须与 src/api/user.api.ts 里实际发出的请求**逐字一致**(前缀也要带上);
7
+ * - 只写路径,不要写协议与主机;
8
+ * - 存成 mock/*.mock.ts 保存即生效,不需要重启 pnpm dev;
9
+ * - 联调真实后端:pnpm dev --no-mock(或 MOCK=none pnpm dev),业务代码不用改。
10
+ *
11
+ * 完整规则见同目录 README.md,约定版本 2026-09。
12
+ */
13
+ export const handlers = [
14
+ http.get('/api/v1/users', async () => {
15
+ await delay(200);
16
+ return HttpResponse.json({
17
+ data: [
18
+ { id: 'u-1', name: '张三', dept: '财务部' },
19
+ { id: 'u-2', name: '李四', dept: '供应链部' }
20
+ ],
21
+ total: 2
22
+ });
23
+ })
24
+ ];
@@ -18,7 +18,7 @@
18
18
  "i18n": "farris-cli extract-i18n"
19
19
  },
20
20
  "dependencies": {
21
- "@farris/ui-vue": "latest",
21
+ "@farris/ui-vue": "<%= uiVueVersion %>",
22
22
  "vue": "^3.4.29",
23
23
  "vue-router": "^4.3.0"
24
24
  },
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Farris 标准 HTTP 请求客户端封装
3
+ */
4
+ export interface RequestOptions extends RequestInit {
5
+ params?: Record<string, any>;
6
+ }
7
+
8
+ export async function request<T = any>(url: string, options: RequestOptions = {}): Promise<T> {
9
+ const { params, ...customConfig } = options;
10
+
11
+ let requestUrl = url;
12
+ if (params && Object.keys(params).length > 0) {
13
+ const searchParams = new URLSearchParams();
14
+ Object.entries(params).forEach(([key, val]) => {
15
+ if (val !== undefined && val !== null) {
16
+ searchParams.append(key, String(val));
17
+ }
18
+ });
19
+ requestUrl += (url.includes('?') ? '&' : '?') + searchParams.toString();
20
+ }
21
+
22
+ const headers = {
23
+ 'Content-Type': 'application/json',
24
+ ...(customConfig.headers || {})
25
+ };
26
+
27
+ const response = await fetch(requestUrl, {
28
+ ...customConfig,
29
+ headers
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const errorText = await response.text().catch(() => response.statusText);
34
+ throw new Error(`HTTP Error ${response.status}: ${errorText}`);
35
+ }
36
+
37
+ return response.json();
38
+ }
@@ -0,0 +1 @@
1
+ export * from './user.api';
@@ -0,0 +1,21 @@
1
+ import { request } from './http-client';
2
+
3
+ export interface UserItem {
4
+ id: string;
5
+ name: string;
6
+ dept: string;
7
+ }
8
+
9
+ export interface UserListResponse {
10
+ data: UserItem[];
11
+ total?: number;
12
+ }
13
+
14
+ /**
15
+ * 用户列表。
16
+ *
17
+ * 开发期由工程内 mock/user.mock.ts 提供假数据;联调真实后端执行 pnpm dev --no-mock。
18
+ */
19
+ export async function getUserList(): Promise<UserListResponse> {
20
+ return request<UserListResponse>('/api/v1/users');
21
+ }
@@ -1,11 +1,43 @@
1
- import { defineComponent } from 'vue';
1
+ import { defineComponent, onMounted, ref } from 'vue';
2
+ import { getUserList, type UserItem } from '../api/user.api';
2
3
 
3
4
  export default defineComponent({
4
5
  name: 'HomeView',
5
6
  setup() {
7
+ const users = ref<UserItem[]>([]);
8
+ const loading = ref(false);
9
+ const error = ref('');
10
+
11
+ async function loadUsers(): Promise<void> {
12
+ loading.value = true;
13
+ error.value = '';
14
+ try {
15
+ users.value = (await getUserList()).data;
16
+ } catch (err) {
17
+ // 后端不可用时不阻塞页面:把失败原因显示出来即可
18
+ error.value = err instanceof Error ? err.message : '请求失败';
19
+ } finally {
20
+ loading.value = false;
21
+ }
22
+ }
23
+
24
+ onMounted(loadUsers);
25
+
6
26
  return () => (
7
27
  <main>
8
28
  <h1>Home</h1>
29
+ <p>下面的数据来自工程内 mock/ 目录的假接口;联调真实后端执行 pnpm dev --no-mock。</p>
30
+ {loading.value ? <p>加载中…</p> : null}
31
+ {!loading.value && error.value ? <p>加载失败:{error.value}</p> : null}
32
+ {!loading.value && !error.value ? (
33
+ <ul>
34
+ {users.value.map((user) => (
35
+ <li key={user.id}>
36
+ {user.name}({user.dept})
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ ) : null}
9
41
  <p>在 src/views/ 下新增页面;通用组件按需创建在 src/components/ 下。</p>
10
42
  </main>
11
43
  );
@@ -4,8 +4,10 @@ import HomeView from '../HomeView';
4
4
 
5
5
  describe('HomeView', () => {
6
6
  it('mounts', () => {
7
+ // 页面挂载时会请求接口(开发期由 mock/ 提供数据),这里只断言首屏能渲染
7
8
  const wrapper = mount(HomeView);
8
9
 
9
10
  expect(wrapper.exists()).toBe(true);
11
+ expect(wrapper.text()).toContain('Home');
10
12
  });
11
13
  });