@aiao/rxdb-plugin-search-react 0.0.23 → 0.0.25
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/README.md +78 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -39
- package/dist/use-search.d.ts +1 -1
- package/dist/use-search.d.ts.map +1 -1
- package/package.json +21 -8
- package/src/index.ts +5 -7
- package/src/use-search.ts +34 -26
- package/REVIEW.md +0 -23
- package/project.json +0 -8
- package/src/index.spec.ts +0 -18
- package/src/use-search.spec.ts +0 -224
- package/tsconfig.json +0 -16
- package/tsconfig.lib.json +0 -36
- package/tsconfig.spec.json +0 -27
- package/vite.config.mts +0 -60
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @aiao/rxdb-plugin-search-react
|
|
2
|
+
|
|
3
|
+
[`@aiao/rxdb-plugin-search`](../rxdb-plugin-search) 的 React 集成层。提供 `useSearch()` hook,将搜索插件的 `SearchHandle` 适配为 React 状态消费接口。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @aiao/rxdb @aiao/rxdb-plugin-search @aiao/rxdb-plugin-search-react react rxjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 用法
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import type { RxDB } from '@aiao/rxdb';
|
|
15
|
+
import { rxDBPluginSearch } from '@aiao/rxdb-plugin-search';
|
|
16
|
+
import { useSearch, type UseSearchReturn } from '@aiao/rxdb-plugin-search-react';
|
|
17
|
+
|
|
18
|
+
export async function startSearch(database: RxDB): Promise<void> {
|
|
19
|
+
database.use(rxDBPluginSearch, { debounce: 200, pageSize: 20 });
|
|
20
|
+
await database.connect('sqlite-wasm');
|
|
21
|
+
await database.searchPlugin.ready;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function SearchPage({ database }: { readonly database: RxDB }) {
|
|
25
|
+
const search: UseSearchReturn = useSearch(database, {
|
|
26
|
+
collections: ['Article'],
|
|
27
|
+
initialQuery: ''
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<main>
|
|
32
|
+
<label>
|
|
33
|
+
Search
|
|
34
|
+
<input value={search.query} onChange={event => search.setQuery(event.currentTarget.value)} />
|
|
35
|
+
</label>
|
|
36
|
+
{search.error && <p role="alert">{search.error.message}</p>}
|
|
37
|
+
{search.state === 'error' && <button onClick={search.retry}>Retry</button>}
|
|
38
|
+
<ul>
|
|
39
|
+
{search.results.map(result => (
|
|
40
|
+
<li key={`${result.collection}:${result.id}`}>{result.snippet}</li>
|
|
41
|
+
))}
|
|
42
|
+
</ul>
|
|
43
|
+
{search.hasMore && <button onClick={() => void search.loadMore()}>Load more</button>}
|
|
44
|
+
<button onClick={search.clear}>Clear</button>
|
|
45
|
+
</main>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`startSearch()` 必须在渲染 `SearchPage` 前完成。`connect()` 会触发数据库 `init()`;
|
|
51
|
+
`database.searchPlugin.ready` 等待 FTS 安装与回填完成。未安装或销毁后访问 `ready` 会 reject,
|
|
52
|
+
不能把它当成永远成功的探针。
|
|
53
|
+
|
|
54
|
+
`useSearch` 返回 `UseSearchReturn`。hook 持有 `SearchHandle` 的所有权,组件卸载或重建时自动
|
|
55
|
+
`destroy()`;调用方不要再次销毁底层 handle。
|
|
56
|
+
|
|
57
|
+
### 重建契约(三端一致)
|
|
58
|
+
|
|
59
|
+
`source` 变化、或 `options` 的 `collections` / `debounce` / `pageSize` / `snippetLength`
|
|
60
|
+
任一变化时,旧 `SearchHandle` 被销毁、新 handle 以**用户当前 query** 播种:
|
|
61
|
+
|
|
62
|
+
- `initialQuery` **只在首次创建时**作种子,此后不再参与重建判据,
|
|
63
|
+
因此传 `initialQuery: query` 不会导致每次击键重建 handle;
|
|
64
|
+
- `options` 按语义(而非引用)比较,每次渲染新建的字面量不触发重建;
|
|
65
|
+
- 重建后 `loadMore` / `clear` / `retry` 一律路由到最新 handle,旧 handle 的晚到 emission 不再写入输出。
|
|
66
|
+
|
|
67
|
+
判据由 core 的 `searchOptionsEqual` 三端共用,重建时机不会因框架而异。
|
|
68
|
+
|
|
69
|
+
三框架同功能对称:Angular 用 `injectSearch`([`@aiao/rxdb-plugin-search-angular`](../rxdb-plugin-search-angular)),Vue 用 `useSearch`([`@aiao/rxdb-plugin-search-vue`](../rxdb-plugin-search-vue))。
|
|
70
|
+
|
|
71
|
+
## 文档
|
|
72
|
+
|
|
73
|
+
- 仓库主页:[https://github.com/aiao-io/rxdb](https://github.com/aiao-io/rxdb)
|
|
74
|
+
- 框架集成与搜索指南见项目文档站
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
[MIT](https://github.com/aiao-io/rxdb/blob/main/LICENSE)
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
*/
|
|
8
|
-
export
|
|
8
|
+
export { SearchExecutionError } from '@aiao/rxdb-plugin-search';
|
|
9
|
+
export type { SearchHandle, SearchOptions, SearchResult, SearchState } from '@aiao/rxdb-plugin-search';
|
|
9
10
|
export { useSearch } from './use-search.js';
|
|
10
11
|
export type { SearchSourceLike, UseSearchReturn } from './use-search.js';
|
|
11
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AAKH,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvG,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,57 +1,58 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { SearchExecutionError as e } from "@aiao/rxdb-plugin-search";
|
|
3
|
+
import { useCallback as t, useEffect as n, useLayoutEffect as r, useMemo as i, useRef as a, useState as o } from "react";
|
|
4
|
+
import { Subscription as s } from "rxjs";
|
|
4
5
|
//#region src/use-search.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
let t = r(e);
|
|
13
|
-
return s(t.current, e) || (t.current = e), t.current;
|
|
14
|
-
}
|
|
15
|
-
function l(o, s) {
|
|
16
|
-
let l = c(s), u = l?.initialQuery ?? "", d = r(null), [f, p] = i(u), [m, h] = i([]), [g, _] = i("idle"), [v, y] = i(void 0), [b, x] = i(!1);
|
|
17
|
-
t(() => {
|
|
18
|
-
let e = o.search(u, l);
|
|
19
|
-
d.current = e, p(u);
|
|
20
|
-
let t = new a();
|
|
21
|
-
return t.add(e.results$.subscribe((e) => h(e))), t.add(e.state$.subscribe((e) => _(e))), t.add(e.error$.subscribe((e) => y(e))), t.add(e.hasMore$.subscribe((e) => x(e))), () => {
|
|
22
|
-
t.unsubscribe(), e.destroy(), d.current === e && (d.current = null);
|
|
23
|
-
};
|
|
6
|
+
var c = typeof window > "u" ? n : r;
|
|
7
|
+
function l(e) {
|
|
8
|
+
let t = e?.collections === void 0 ? void 0 : JSON.stringify(e.collections);
|
|
9
|
+
return i(() => {
|
|
10
|
+
if (e === void 0) return;
|
|
11
|
+
let t = {};
|
|
12
|
+
return e.debounce !== void 0 && (t.debounce = e.debounce), e.pageSize !== void 0 && (t.pageSize = e.pageSize), e.snippetLength !== void 0 && (t.snippetLength = e.snippetLength), e.collections !== void 0 && (t.collections = [...e.collections]), e.initialQuery !== void 0 && (t.initialQuery = e.initialQuery), t;
|
|
24
13
|
}, [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
e?.debounce,
|
|
15
|
+
e?.pageSize,
|
|
16
|
+
e?.snippetLength,
|
|
17
|
+
t
|
|
28
18
|
]);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
}
|
|
20
|
+
function u(e, n) {
|
|
21
|
+
let r = l(n), u = r?.initialQuery ?? "", d = a(null), [f, p] = o(u), [m, h] = o([]), [g, _] = o("idle"), [v, y] = o(void 0), [b, x] = o(!1), S = a(u);
|
|
22
|
+
c(() => {
|
|
23
|
+
let t = S.current, n = e.search(t, r);
|
|
24
|
+
d.current = n;
|
|
25
|
+
let i = new s();
|
|
26
|
+
return i.add(n.results$.subscribe((e) => h(e))), i.add(n.state$.subscribe((e) => _(e))), i.add(n.error$.subscribe((e) => y(e))), i.add(n.hasMore$.subscribe((e) => x(e))), () => {
|
|
27
|
+
i.unsubscribe(), n.destroy(), d.current === n && (d.current = null);
|
|
28
|
+
};
|
|
29
|
+
}, [e, r]);
|
|
30
|
+
let C = t((e) => {
|
|
31
|
+
S.current = e, p(e), d.current?.setQuery(e);
|
|
32
|
+
}, []), w = t(() => {
|
|
33
|
+
S.current = "", p(""), d.current?.clear();
|
|
34
|
+
}, []), T = t(() => d.current?.retry(), []), E = t(() => d.current?.loadMore() ?? Promise.resolve(), []);
|
|
35
|
+
return i(() => ({
|
|
35
36
|
query: f,
|
|
36
|
-
setQuery:
|
|
37
|
+
setQuery: C,
|
|
37
38
|
results: m,
|
|
38
39
|
state: g,
|
|
39
40
|
error: v,
|
|
40
41
|
hasMore: b,
|
|
41
|
-
loadMore:
|
|
42
|
-
clear:
|
|
43
|
-
retry:
|
|
42
|
+
loadMore: E,
|
|
43
|
+
clear: w,
|
|
44
|
+
retry: T
|
|
44
45
|
}), [
|
|
45
46
|
f,
|
|
46
|
-
|
|
47
|
+
C,
|
|
47
48
|
m,
|
|
48
49
|
g,
|
|
49
50
|
v,
|
|
50
51
|
b,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
E,
|
|
53
|
+
w,
|
|
54
|
+
T
|
|
54
55
|
]);
|
|
55
56
|
}
|
|
56
57
|
//#endregion
|
|
57
|
-
export {
|
|
58
|
+
export { e as SearchExecutionError, u as useSearch };
|
package/dist/use-search.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { SearchExecutionError, SearchOptions, SearchResult, SearchSourceLike, Se
|
|
|
3
3
|
* React 集成 - rxdb-plugin-search
|
|
4
4
|
*
|
|
5
5
|
* 提供 `useSearch()`,将 `SearchHandle` 的 Observable 适配为 React state;
|
|
6
|
-
*
|
|
6
|
+
* 通过同构 layout effect 的清理函数在组件卸载时自动释放 handle。
|
|
7
7
|
*
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
package/dist/use-search.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-search.d.ts","sourceRoot":"","sources":["../src/use-search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,oBAAoB,EAEpB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC;;;;;;;GAOG;AAEH,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,WAAW;IACX,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,WAAW;IACX,KAAK,EAAE,WAAW,CAAC;IACnB,eAAe;IACf,KAAK,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACxC,cAAc;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,sBAAsB;IACtB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,oBAAoB;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;
|
|
1
|
+
{"version":3,"file":"use-search.d.ts","sourceRoot":"","sources":["../src/use-search.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,oBAAoB,EAEpB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC;;;;;;;GAOG;AAEH,YAAY,EAAE,gBAAgB,EAAE,CAAC;AAEjC;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,YAAY;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,WAAW;IACX,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,WAAW;IACX,KAAK,EAAE,WAAW,CAAC;IACnB,eAAe;IACf,KAAK,EAAE,oBAAoB,GAAG,SAAS,CAAC;IACxC,cAAc;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,sBAAsB;IACtB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,oBAAoB;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAkBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,eAAe,CAgE5F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiao/rxdb-plugin-search-react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/aiao-io/rxdb.git",
|
|
8
|
+
"directory": "packages/rxdb-plugin-search-react"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src",
|
|
13
|
+
"!src/**/*.spec.*",
|
|
14
|
+
"!src/**/*.test.*",
|
|
15
|
+
"!src/**/__tests__/**",
|
|
16
|
+
"!**/*.tsbuildinfo"
|
|
17
|
+
],
|
|
4
18
|
"type": "module",
|
|
5
19
|
"main": "./dist/index.js",
|
|
6
20
|
"module": "./dist/index.js",
|
|
@@ -8,20 +22,19 @@
|
|
|
8
22
|
"exports": {
|
|
9
23
|
"./package.json": "./package.json",
|
|
10
24
|
".": {
|
|
11
|
-
"@aiao/source": "./src/index.ts",
|
|
12
25
|
"types": "./dist/index.d.ts",
|
|
13
26
|
"import": "./dist/index.js",
|
|
14
27
|
"default": "./dist/index.js"
|
|
15
28
|
}
|
|
16
29
|
},
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@aiao/rxdb-plugin-search": "0.0.23"
|
|
19
|
-
},
|
|
20
30
|
"peerDependencies": {
|
|
21
31
|
"@aiao/rxdb": "*",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
32
|
+
"@aiao/rxdb-plugin-search": "*",
|
|
33
|
+
"react": "^19.2.8",
|
|
34
|
+
"rxjs": "^7.8.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@aiao/rxdb-plugin-search": "0.0.25"
|
|
25
38
|
},
|
|
26
39
|
"publishConfig": {
|
|
27
40
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
* @packageDocumentation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
SearchState
|
|
17
|
-
} from '@aiao/rxdb-plugin-search';
|
|
11
|
+
// `SearchExecutionError` 是 core 的**运行时 class**,必须作为值再导出:
|
|
12
|
+
// 混在 `export type` 里会让它在真实 ESM 中不存在,消费者拿到 `error` 后
|
|
13
|
+
// 无法 `instanceof`,只能额外从 core import(SRCHR-006,三端同款)。
|
|
14
|
+
export { SearchExecutionError } from '@aiao/rxdb-plugin-search';
|
|
15
|
+
export type { SearchHandle, SearchOptions, SearchResult, SearchState } from '@aiao/rxdb-plugin-search';
|
|
18
16
|
export { useSearch } from './use-search.js';
|
|
19
17
|
export type { SearchSourceLike, UseSearchReturn } from './use-search.js';
|
package/src/use-search.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Subscription } from 'rxjs';
|
|
5
5
|
|
|
6
6
|
import type {
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
* React 集成 - rxdb-plugin-search
|
|
17
17
|
*
|
|
18
18
|
* 提供 `useSearch()`,将 `SearchHandle` 的 Observable 适配为 React state;
|
|
19
|
-
*
|
|
19
|
+
* 通过同构 layout effect 的清理函数在组件卸载时自动释放 handle。
|
|
20
20
|
*
|
|
21
21
|
* @packageDocumentation
|
|
22
22
|
*/
|
|
@@ -49,28 +49,20 @@ export interface UseSearchReturn {
|
|
|
49
49
|
retry: () => void;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
if (left === right) return true;
|
|
54
|
-
if (left === undefined || right === undefined || left.length !== right.length) return false;
|
|
55
|
-
return left.every((value, index) => value === right[index]);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function searchOptionsEqual(left: SearchOptions | undefined, right: SearchOptions | undefined): boolean {
|
|
59
|
-
if (left === right) return true;
|
|
60
|
-
if (left === undefined || right === undefined) return false;
|
|
61
|
-
return (
|
|
62
|
-
left.debounce === right.debounce &&
|
|
63
|
-
left.pageSize === right.pageSize &&
|
|
64
|
-
left.snippetLength === right.snippetLength &&
|
|
65
|
-
left.initialQuery === right.initialQuery &&
|
|
66
|
-
collectionsEqual(left.collections, right.collections)
|
|
67
|
-
);
|
|
68
|
-
}
|
|
52
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
69
53
|
|
|
70
54
|
function useStableSearchOptions(options: SearchOptions | undefined): SearchOptions | undefined {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
const collectionsKey = options?.collections === undefined ? undefined : JSON.stringify(options.collections);
|
|
56
|
+
return useMemo(() => {
|
|
57
|
+
if (options === undefined) return undefined;
|
|
58
|
+
const snapshot: SearchOptions = {};
|
|
59
|
+
if (options.debounce !== undefined) snapshot.debounce = options.debounce;
|
|
60
|
+
if (options.pageSize !== undefined) snapshot.pageSize = options.pageSize;
|
|
61
|
+
if (options.snippetLength !== undefined) snapshot.snippetLength = options.snippetLength;
|
|
62
|
+
if (options.collections !== undefined) snapshot.collections = [...options.collections];
|
|
63
|
+
if (options.initialQuery !== undefined) snapshot.initialQuery = options.initialQuery;
|
|
64
|
+
return snapshot;
|
|
65
|
+
}, [options?.debounce, options?.pageSize, options?.snippetLength, collectionsKey]);
|
|
74
66
|
}
|
|
75
67
|
|
|
76
68
|
/**
|
|
@@ -97,13 +89,27 @@ export function useSearch(source: SearchSourceLike, options?: SearchOptions): Us
|
|
|
97
89
|
const [error, setError] = useState<SearchExecutionError | undefined>(undefined);
|
|
98
90
|
const [hasMore, setHasMore] = useState<boolean>(false);
|
|
99
91
|
|
|
100
|
-
|
|
92
|
+
/**
|
|
93
|
+
* 当前 query 的「最新值」镜像,供重建时播种新 handle。
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* SRCHR-001:早先重建时无条件 `setQueryState(initial)`,`initial` 只来自
|
|
97
|
+
* `options.initialQuery` —— 于是「切换搜索范围」这种与输入框无关的操作
|
|
98
|
+
* 会把用户打了一半的词清空。契约改为**重建保留当前 query**。
|
|
99
|
+
*
|
|
100
|
+
* 用 ref 而不是把 `query` 加进 effect 依赖:后者会让每次击键都重建 handle,
|
|
101
|
+
* 防抖与分页全废。写入点只有 `setQuery` / `clear` 两个回调(非渲染期),
|
|
102
|
+
* layout effect 负责在消费者同阶段 effect 前完成句柄绑定,避免首个 commit 的命令窗口。
|
|
103
|
+
*/
|
|
104
|
+
const queryRef = useRef<string>(initial);
|
|
105
|
+
|
|
106
|
+
useIsomorphicLayoutEffect(() => {
|
|
101
107
|
// handle 是命令式控制器,不参与渲染,存入 ref 而非 state:
|
|
102
108
|
// 避免 handle 变化触发多余的重渲染,并让 setQuery/clear/retry/loadMore
|
|
103
109
|
// 始终读取最新 handle,不会在 rebuild 后仍调用到已销毁的旧实例。
|
|
104
|
-
const
|
|
110
|
+
const seed = queryRef.current;
|
|
111
|
+
const newHandle = source.search(seed, stableOptions);
|
|
105
112
|
handleRef.current = newHandle;
|
|
106
|
-
setQueryState(initial);
|
|
107
113
|
|
|
108
114
|
const subs = new Subscription();
|
|
109
115
|
subs.add(newHandle.results$.subscribe(r => setResults(r)));
|
|
@@ -115,14 +121,16 @@ export function useSearch(source: SearchSourceLike, options?: SearchOptions): Us
|
|
|
115
121
|
newHandle.destroy();
|
|
116
122
|
if (handleRef.current === newHandle) handleRef.current = null;
|
|
117
123
|
};
|
|
118
|
-
}, [source, stableOptions
|
|
124
|
+
}, [source, stableOptions]);
|
|
119
125
|
|
|
120
126
|
const setQuery = useCallback((q: string) => {
|
|
127
|
+
queryRef.current = q;
|
|
121
128
|
setQueryState(q);
|
|
122
129
|
handleRef.current?.setQuery(q);
|
|
123
130
|
}, []);
|
|
124
131
|
|
|
125
132
|
const clear = useCallback(() => {
|
|
133
|
+
queryRef.current = '';
|
|
126
134
|
setQueryState('');
|
|
127
135
|
handleRef.current?.clear();
|
|
128
136
|
}, []);
|
package/REVIEW.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# `@aiao/rxdb-plugin-search-react` 代码评审
|
|
2
|
-
|
|
3
|
-
## 结论
|
|
4
|
-
|
|
5
|
-
🔴 不通过。`useSearch()` 宣称 source 变化时重建 handle,但实际把首次 source 永久锁进 ref;组件切换数据库/collection 后仍订阅和查询旧 source。
|
|
6
|
-
|
|
7
|
-
## 评审基线
|
|
8
|
-
|
|
9
|
-
- 基线提交:`03a46a5d5992a958c19ae33d5fed15c9c3322021`
|
|
10
|
-
- 评审日期:2026-07-14
|
|
11
|
-
- 范围:React 搜索 hook、测试、公开入口;10 个文件,约 311 行 TS
|
|
12
|
-
- 自动校验:`lint`、`test`、`typecheck`、`build` 全部通过
|
|
13
|
-
|
|
14
|
-
## 问题
|
|
15
|
-
|
|
16
|
-
| ID | 级别 | 位置 | 问题与影响 | 建议 |
|
|
17
|
-
| --- | --- | --- | --- | --- |
|
|
18
|
-
| SEARCH-REACT-001 | P1 | `src/use-search.ts:67` | `handleRef` 只在 null 时用 source 创建,订阅 effect 的依赖数组为空;rerender 传入新 source/options 时,hook 仍对旧 handle 调用 setQuery/loadMore,并显示旧数据库结果。源码注释“仅在 source 变化时重建”与实现相反。 | 用 source/稳定 options key 作为 handle 生命周期依赖,切换时先取消旧订阅并 destroy 旧 handle;增加 rerender source A→B 后只能调用 B 的测试。 |
|
|
19
|
-
|
|
20
|
-
## 三端对称性
|
|
21
|
-
|
|
22
|
-
- 共享类型透传与 Vue/Angular 一致;运行时入口 `useSearch` 符合 React 约定。
|
|
23
|
-
- source 切换语义目前不可靠,是此三端组唯一 P1。
|
package/project.json
DELETED
package/src/index.spec.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import type { SearchExecutionError, SearchHandle, SearchOptions, SearchResult, SearchState } from './index.js';
|
|
4
|
-
|
|
5
|
-
type PublicSearchApi = {
|
|
6
|
-
error: SearchExecutionError | undefined;
|
|
7
|
-
handle: SearchHandle;
|
|
8
|
-
options: SearchOptions;
|
|
9
|
-
result: SearchResult;
|
|
10
|
-
state: SearchState;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
describe('React public search API', () => {
|
|
14
|
-
it('exposes the core search types from the package entrypoint', () => {
|
|
15
|
-
const contract: PublicSearchApi | undefined = undefined;
|
|
16
|
-
expect(contract).toBeUndefined();
|
|
17
|
-
});
|
|
18
|
-
});
|
package/src/use-search.spec.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
|
|
3
|
-
import { act, renderHook } from '@testing-library/react';
|
|
4
|
-
import { StrictMode, createElement, type PropsWithChildren } from 'react';
|
|
5
|
-
import { BehaviorSubject } from 'rxjs';
|
|
6
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
-
|
|
8
|
-
import type { SearchExecutionError, SearchHandle, SearchResult, SearchState } from '@aiao/rxdb-plugin-search';
|
|
9
|
-
|
|
10
|
-
import { useSearch, type SearchSourceLike } from '../src/use-search.js';
|
|
11
|
-
|
|
12
|
-
const makeHandle = () => {
|
|
13
|
-
const results$ = new BehaviorSubject<SearchResult[]>([]);
|
|
14
|
-
const state$ = new BehaviorSubject<SearchState>('idle');
|
|
15
|
-
const error$ = new BehaviorSubject<SearchExecutionError | undefined>(undefined);
|
|
16
|
-
const hasMore$ = new BehaviorSubject<boolean>(false);
|
|
17
|
-
const destroyed = { value: false };
|
|
18
|
-
const setQuery = vi.fn();
|
|
19
|
-
const loadMore = vi.fn(async () => {
|
|
20
|
-
/* noop */
|
|
21
|
-
});
|
|
22
|
-
const clear = vi.fn();
|
|
23
|
-
const retry = vi.fn();
|
|
24
|
-
|
|
25
|
-
const handle: SearchHandle = {
|
|
26
|
-
results$,
|
|
27
|
-
state$,
|
|
28
|
-
error$,
|
|
29
|
-
hasMore$,
|
|
30
|
-
setQuery,
|
|
31
|
-
loadMore,
|
|
32
|
-
clear,
|
|
33
|
-
retry,
|
|
34
|
-
destroy: vi.fn(() => {
|
|
35
|
-
destroyed.value = true;
|
|
36
|
-
})
|
|
37
|
-
};
|
|
38
|
-
return { handle, results$, state$, error$, hasMore$, setQuery, loadMore, clear, retry, destroyed };
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const makeSource = (handle: SearchHandle): SearchSourceLike & { searchSpy: ReturnType<typeof vi.fn> } => {
|
|
42
|
-
const searchSpy = vi.fn(() => handle);
|
|
43
|
-
return {
|
|
44
|
-
search: searchSpy,
|
|
45
|
-
searchSpy
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
vi.useRealTimers();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('useSearch (React binding, T039)', () => {
|
|
54
|
-
it('initial render mirrors handle observables', () => {
|
|
55
|
-
const h = makeHandle();
|
|
56
|
-
const src = makeSource(h.handle);
|
|
57
|
-
const { result } = renderHook(() => useSearch(src));
|
|
58
|
-
expect(result.current.query).toBe('');
|
|
59
|
-
expect(result.current.results).toEqual([]);
|
|
60
|
-
expect(result.current.state).toBe('idle');
|
|
61
|
-
expect(result.current.hasMore).toBe(false);
|
|
62
|
-
expect(src.searchSpy).toHaveBeenCalledTimes(1);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('setQuery forwards to handle.setQuery and updates local query state', () => {
|
|
66
|
-
const h = makeHandle();
|
|
67
|
-
const src = makeSource(h.handle);
|
|
68
|
-
const { result } = renderHook(() => useSearch(src));
|
|
69
|
-
act(() => result.current.setQuery('foo'));
|
|
70
|
-
expect(h.setQuery).toHaveBeenCalledWith('foo');
|
|
71
|
-
expect(result.current.query).toBe('foo');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('handle observable updates propagate to hook state', () => {
|
|
75
|
-
const h = makeHandle();
|
|
76
|
-
const src = makeSource(h.handle);
|
|
77
|
-
const { result } = renderHook(() => useSearch(src));
|
|
78
|
-
const rows: SearchResult[] = [{ entity: 'A', collection: 'a', id: '1', rank: -1, matchedField: 'x', snippet: 's' }];
|
|
79
|
-
act(() => {
|
|
80
|
-
h.results$.next(rows);
|
|
81
|
-
h.state$.next('success');
|
|
82
|
-
h.hasMore$.next(true);
|
|
83
|
-
});
|
|
84
|
-
expect(result.current.results).toEqual(rows);
|
|
85
|
-
expect(result.current.state).toBe('success');
|
|
86
|
-
expect(result.current.hasMore).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('unmount calls handle.destroy', () => {
|
|
90
|
-
vi.useFakeTimers();
|
|
91
|
-
const h = makeHandle();
|
|
92
|
-
const src = makeSource(h.handle);
|
|
93
|
-
const { unmount } = renderHook(() => useSearch(src));
|
|
94
|
-
unmount();
|
|
95
|
-
vi.runAllTimers();
|
|
96
|
-
expect(h.destroyed.value).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('keeps the handle usable under React StrictMode double effects', () => {
|
|
100
|
-
vi.useFakeTimers();
|
|
101
|
-
const h = makeHandle();
|
|
102
|
-
const src = makeSource(h.handle);
|
|
103
|
-
const StrictWrapper = ({ children }: PropsWithChildren) => createElement(StrictMode, null, children);
|
|
104
|
-
|
|
105
|
-
const { result, unmount } = renderHook(() => useSearch(src), { wrapper: StrictWrapper });
|
|
106
|
-
|
|
107
|
-
act(() => {
|
|
108
|
-
vi.runAllTimers();
|
|
109
|
-
});
|
|
110
|
-
expect(h.destroyed.value).toBe(false);
|
|
111
|
-
|
|
112
|
-
act(() => result.current.setQuery('foo'));
|
|
113
|
-
expect(h.setQuery).toHaveBeenCalledWith('foo');
|
|
114
|
-
expect(result.current.query).toBe('foo');
|
|
115
|
-
|
|
116
|
-
unmount();
|
|
117
|
-
act(() => {
|
|
118
|
-
vi.runAllTimers();
|
|
119
|
-
});
|
|
120
|
-
expect(h.destroyed.value).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('clear() resets query to empty and calls handle.clear', () => {
|
|
124
|
-
const h = makeHandle();
|
|
125
|
-
const src = makeSource(h.handle);
|
|
126
|
-
const { result } = renderHook(() => useSearch(src));
|
|
127
|
-
act(() => result.current.setQuery('foo'));
|
|
128
|
-
act(() => result.current.clear());
|
|
129
|
-
expect(h.clear).toHaveBeenCalledTimes(1);
|
|
130
|
-
expect(result.current.query).toBe('');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('retry() and loadMore() forward to handle', async () => {
|
|
134
|
-
const h = makeHandle();
|
|
135
|
-
const src = makeSource(h.handle);
|
|
136
|
-
const { result } = renderHook(() => useSearch(src));
|
|
137
|
-
act(() => result.current.retry());
|
|
138
|
-
await act(async () => {
|
|
139
|
-
await result.current.loadMore();
|
|
140
|
-
});
|
|
141
|
-
expect(h.retry).toHaveBeenCalledTimes(1);
|
|
142
|
-
expect(h.loadMore).toHaveBeenCalledTimes(1);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('passes initialQuery to source.search', () => {
|
|
146
|
-
const h = makeHandle();
|
|
147
|
-
const src = makeSource(h.handle);
|
|
148
|
-
renderHook(() => useSearch(src, { initialQuery: 'hi' }));
|
|
149
|
-
expect(src.searchSpy).toHaveBeenCalledWith('hi', { initialQuery: 'hi' });
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('rebuilds the handle when source changes', () => {
|
|
153
|
-
vi.useFakeTimers();
|
|
154
|
-
const first = makeHandle();
|
|
155
|
-
const second = makeHandle();
|
|
156
|
-
const firstSource = makeSource(first.handle);
|
|
157
|
-
const secondSource = makeSource(second.handle);
|
|
158
|
-
const { result, rerender } = renderHook(({ source }) => useSearch(source), {
|
|
159
|
-
initialProps: { source: firstSource as SearchSourceLike }
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
rerender({ source: secondSource });
|
|
163
|
-
act(() => vi.runAllTimers());
|
|
164
|
-
act(() => result.current.setQuery('next'));
|
|
165
|
-
|
|
166
|
-
expect(first.destroyed.value).toBe(true);
|
|
167
|
-
expect(first.setQuery).not.toHaveBeenCalled();
|
|
168
|
-
expect(secondSource.searchSpy).toHaveBeenCalledTimes(1);
|
|
169
|
-
expect(second.setQuery).toHaveBeenCalledWith('next');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('does not rebuild for a semantically identical inline options object', () => {
|
|
173
|
-
vi.useFakeTimers();
|
|
174
|
-
const h = makeHandle();
|
|
175
|
-
const src = makeSource(h.handle);
|
|
176
|
-
const { rerender } = renderHook(() => useSearch(src, { debounce: 10, collections: ['Todo'] }));
|
|
177
|
-
|
|
178
|
-
rerender();
|
|
179
|
-
act(() => vi.runAllTimers());
|
|
180
|
-
|
|
181
|
-
expect(src.searchSpy).toHaveBeenCalledTimes(1);
|
|
182
|
-
expect(h.destroyed.value).toBe(false);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('routes calls to the latest handle even via a callback captured before rebuild', () => {
|
|
186
|
-
vi.useFakeTimers();
|
|
187
|
-
const first = makeHandle();
|
|
188
|
-
const second = makeHandle();
|
|
189
|
-
const firstSource = makeSource(first.handle);
|
|
190
|
-
const secondSource = makeSource(second.handle);
|
|
191
|
-
const { result, rerender } = renderHook(({ source }) => useSearch(source), {
|
|
192
|
-
initialProps: { source: firstSource as SearchSourceLike }
|
|
193
|
-
});
|
|
194
|
-
const staleSetQuery = result.current.setQuery;
|
|
195
|
-
|
|
196
|
-
rerender({ source: secondSource });
|
|
197
|
-
act(() => vi.runAllTimers());
|
|
198
|
-
|
|
199
|
-
act(() => staleSetQuery('after-rebuild'));
|
|
200
|
-
|
|
201
|
-
expect(second.setQuery).toHaveBeenCalledWith('after-rebuild');
|
|
202
|
-
expect(first.setQuery).not.toHaveBeenCalled();
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('keeps stable setQuery/clear/retry/loadMore identities across handle rebuilds', () => {
|
|
206
|
-
vi.useFakeTimers();
|
|
207
|
-
const first = makeHandle();
|
|
208
|
-
const second = makeHandle();
|
|
209
|
-
const firstSource = makeSource(first.handle);
|
|
210
|
-
const secondSource = makeSource(second.handle);
|
|
211
|
-
const { result, rerender } = renderHook(({ source }) => useSearch(source), {
|
|
212
|
-
initialProps: { source: firstSource as SearchSourceLike }
|
|
213
|
-
});
|
|
214
|
-
const { setQuery, clear, retry, loadMore } = result.current;
|
|
215
|
-
|
|
216
|
-
rerender({ source: secondSource });
|
|
217
|
-
act(() => vi.runAllTimers());
|
|
218
|
-
|
|
219
|
-
expect(result.current.setQuery).toBe(setQuery);
|
|
220
|
-
expect(result.current.clear).toBe(clear);
|
|
221
|
-
expect(result.current.retry).toBe(retry);
|
|
222
|
-
expect(result.current.loadMore).toBe(loadMore);
|
|
223
|
-
});
|
|
224
|
-
});
|
package/tsconfig.json
DELETED
package/tsconfig.lib.json
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"outDir": "dist",
|
|
4
|
-
"types": ["node", "vite/client"],
|
|
5
|
-
"rootDir": "src",
|
|
6
|
-
"jsx": "react-jsx",
|
|
7
|
-
"module": "esnext",
|
|
8
|
-
"moduleResolution": "bundler",
|
|
9
|
-
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
|
|
10
|
-
},
|
|
11
|
-
"exclude": [
|
|
12
|
-
"out-tsc",
|
|
13
|
-
"dist",
|
|
14
|
-
"**/*.spec.ts",
|
|
15
|
-
"**/*.test.ts",
|
|
16
|
-
"**/*.spec.tsx",
|
|
17
|
-
"**/*.test.tsx",
|
|
18
|
-
"vite.config.ts",
|
|
19
|
-
"vite.config.mts",
|
|
20
|
-
"vitest.config.ts",
|
|
21
|
-
"vitest.config.mts",
|
|
22
|
-
"eslint.config.js",
|
|
23
|
-
"eslint.config.cjs",
|
|
24
|
-
"eslint.config.mjs"
|
|
25
|
-
],
|
|
26
|
-
"extends": "../../tsconfig.base.json",
|
|
27
|
-
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
|
28
|
-
"references": [
|
|
29
|
-
{
|
|
30
|
-
"path": "../rxdb-plugin-search/tsconfig.lib.json"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
"path": "../rxdb/tsconfig.lib.json"
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
}
|
package/tsconfig.spec.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"outDir": "./out-tsc/vitest",
|
|
4
|
-
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"],
|
|
5
|
-
"jsx": "react-jsx",
|
|
6
|
-
"module": "esnext",
|
|
7
|
-
"moduleResolution": "bundler",
|
|
8
|
-
"rootDir": "."
|
|
9
|
-
},
|
|
10
|
-
"extends": "../../tsconfig.base.json",
|
|
11
|
-
"include": [
|
|
12
|
-
"vite.config.ts",
|
|
13
|
-
"vite.config.mts",
|
|
14
|
-
"vitest.config.ts",
|
|
15
|
-
"vitest.config.mts",
|
|
16
|
-
"src/**/*.test.ts",
|
|
17
|
-
"src/**/*.spec.ts",
|
|
18
|
-
"src/**/*.test.tsx",
|
|
19
|
-
"src/**/*.spec.tsx",
|
|
20
|
-
"src/**/*.d.ts"
|
|
21
|
-
],
|
|
22
|
-
"references": [
|
|
23
|
-
{
|
|
24
|
-
"path": "./tsconfig.lib.json"
|
|
25
|
-
}
|
|
26
|
-
]
|
|
27
|
-
}
|
package/vite.config.mts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/// <reference types='vitest' />
|
|
2
|
-
import { codecovVitePlugin } from '@codecov/vite-plugin';
|
|
3
|
-
import react from '@vitejs/plugin-react';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import { defineConfig } from 'vite';
|
|
6
|
-
import dts from 'vite-plugin-dts';
|
|
7
|
-
|
|
8
|
-
export default defineConfig(() => ({
|
|
9
|
-
root: import.meta.dirname,
|
|
10
|
-
cacheDir: '../../node_modules/.vite/packages/rxdb-plugin-search-react',
|
|
11
|
-
plugins: [
|
|
12
|
-
react(),
|
|
13
|
-
dts({
|
|
14
|
-
entryRoot: 'src',
|
|
15
|
-
pathsToAliases: false,
|
|
16
|
-
tsconfigPath: path.join(import.meta.dirname, 'tsconfig.lib.json')
|
|
17
|
-
}),
|
|
18
|
-
...(process.env.CI === 'true' && process.env.CODECOV_TOKEN ?
|
|
19
|
-
[
|
|
20
|
-
codecovVitePlugin({
|
|
21
|
-
enableBundleAnalysis: true,
|
|
22
|
-
telemetry: false,
|
|
23
|
-
bundleName: 'rxdb-plugin-search-react',
|
|
24
|
-
uploadToken: process.env.CODECOV_TOKEN
|
|
25
|
-
})
|
|
26
|
-
]
|
|
27
|
-
: [])
|
|
28
|
-
],
|
|
29
|
-
build: {
|
|
30
|
-
outDir: './dist',
|
|
31
|
-
emptyOutDir: true,
|
|
32
|
-
reportCompressedSize: true,
|
|
33
|
-
commonjsOptions: {
|
|
34
|
-
transformMixedEsModules: true
|
|
35
|
-
},
|
|
36
|
-
lib: {
|
|
37
|
-
entry: 'src/index.ts',
|
|
38
|
-
name: 'rxdb-plugin-search-react',
|
|
39
|
-
fileName: 'index',
|
|
40
|
-
formats: ['es' as const]
|
|
41
|
-
},
|
|
42
|
-
rolldownOptions: {
|
|
43
|
-
// dts 插件生成声明文件天然比 Rolldown 原生链接阶段慢,抑制误报的 PLUGIN_TIMINGS 警告
|
|
44
|
-
checks: { pluginTimings: false },
|
|
45
|
-
external: [/^@aiao\//, 'react', 'react-dom', 'react/jsx-runtime', 'rxjs']
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
test: {
|
|
49
|
-
name: 'rxdb-plugin-search-react',
|
|
50
|
-
watch: false,
|
|
51
|
-
globals: true,
|
|
52
|
-
environment: 'jsdom',
|
|
53
|
-
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
54
|
-
reporters: ['default'],
|
|
55
|
-
coverage: {
|
|
56
|
-
reportsDirectory: './test-output/vitest/coverage',
|
|
57
|
-
provider: 'v8' as const
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}));
|