rspress-plugin-typesense 0.0.2 → 0.1.0

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 CHANGED
@@ -2,21 +2,289 @@
2
2
 
3
3
  A plugin that brings lightning-fast, typo-tolerant search powered by Typesense to your Rspress site.
4
4
 
5
- ## Getting started
5
+ ## About Typesense & Rspress
6
+
7
+ [**Typesense**](https://typesense.org/) is an open-source, lightning-fast search engine that delivers instant, typo-tolerant results with minimal setup. It's an open source alternative to Algolia and an easier-to-use alternative to ElasticSearch.
8
+
9
+ [**Rspress**](https://rspress.rs/) is a high-performance static site generator for documentation websites. Built with Rust, it offers a modern development experience and produces blazing-fast static sites.
10
+
11
+ Together, **Typesense** and **Rspress** provide a seamless way to add powerful, blazingly-fast search to modern documentation websites.
6
12
 
7
- Install dependencies:
13
+ ## Installation
8
14
 
9
15
  ```bash
10
16
  npm install rspress-plugin-typesense
11
17
  ```
12
18
 
13
- ## About Typesense & Rspress
19
+ ## Usage
14
20
 
15
- [**Typesense**](https://typesense.org/) is an open-source, lightning-fast search engine that delivers instant, typo-tolerant results with minimal setup. It's an open source alternative to Algolia and an easier-to-use alternative to ElasticSearch.
21
+ ### 1. Start typesense server
16
22
 
17
- [**Rspress**](https://rspress.rs/) is a high-performance static site generator for documentation websites. Built with Rust, it offers a modern development experience and produces blazing-fast static sites.
23
+ You can either self-host the Typesense server or use [Typesense Cloud service](https://cloud.typesense.org/). Follow this [getting started guide](https://typesense.org/docs/guide/install-typesense.html) to set up your server and obtain the API key and server URL.
18
24
 
19
- Together, **Typesense** and **Rspress** provide a seamless way to add powerful, blazingly-fast search to modern documentation websites.
25
+ ### 2. Configure the plugin
26
+
27
+ First, add the plugin to your `rspress.config.ts`. You must provide your Typesense server details and an API key with **write permissions** so the plugin can create collections and index your documents during the build.
28
+
29
+ ```ts
30
+ // rspress.config.ts
31
+ import { defineConfig } from '@rspress/core';
32
+ import { pluginTypesense } from 'rspress-plugin-typesense';
33
+
34
+ export default defineConfig({
35
+ plugins: [
36
+ pluginTypesense({
37
+ collectionName: 'my_docs',
38
+ serverConfig: {
39
+ nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }],
40
+ apiKey: 'YOUR_TYPESENSE_ADMIN_API_KEY', // Requires Write permissions
41
+ },
42
+ }),
43
+ ],
44
+ });
45
+ ```
46
+
47
+ ### 3. Override the search component
48
+
49
+ Next, override Rspress's default `Search` component via a [Custom Theme](https://rspress.rs/guide/basic/custom-theme).
50
+
51
+ Provide a **Search-Only API Key** here. For security reasons, **never expose your Admin API Key in the frontend**.
52
+
53
+ ```tsx
54
+ // theme/index.tsx
55
+ import { Search as PluginTypesenseSearch } from 'rspress-plugin-typesense/runtime';
56
+
57
+ const Search = () => {
58
+ return (
59
+ <PluginTypesenseSearch
60
+ docSearchProps={{
61
+ typesenseServerConfig: {
62
+ nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }],
63
+ apiKey: 'YOUR_TYPESENSE_SEARCH_ONLY_API_KEY', // Safe for browsers
64
+ },
65
+ }}
66
+ />
67
+ );
68
+ };
69
+
70
+ export { Search };
71
+ export * from '@rspress/core/theme-original';
72
+ ```
73
+
74
+ ### 4. Build and index
75
+
76
+ Run the build command to generate your site and index your content into Typesense.
77
+
78
+ <PackageManagerTabs command="run build" />
79
+
80
+ All set! You've successfully integrated typo-tolerant search into your documentation. The plugin automatically handles filtering based on the user's current language and version.
81
+
82
+ ## Plugin configuration (Backend)
83
+
84
+ The `pluginTypesense` function accepts an options object with the following properties:
85
+
86
+ ```ts
87
+ export interface TypesensePluginOptions {
88
+ /**
89
+ * Typesense server connection options.
90
+ * The API key must have write permissions to create and index collections.
91
+ */
92
+ serverConfig: ConfigurationOptions;
93
+
94
+ /**
95
+ * The base name of the Typesense collection.
96
+ * Note: The plugin creates dedicated localized collections (e.g., `my_docs_en`).
97
+ */
98
+ collectionName: string;
99
+
100
+ /**
101
+ * Optional schema overrides. Can be a global settings object or a map keyed by language.
102
+ */
103
+ customCollectionSettings?: CustomCollectionSettingsConfig;
104
+
105
+ /**
106
+ * Whether to index code blocks into Typesense.
107
+ * Defaults to `false` to avoid search noise and bloated index sizes.
108
+ */
109
+ indexCodeBlocks?: boolean;
110
+
111
+ /**
112
+ * Whether to automatically filter search results by the active documentation version.
113
+ * Defaults to `true`.
114
+ */
115
+ versionedSearch?: boolean;
116
+ }
117
+ ```
118
+
119
+ ### customCollectionSettings
120
+
121
+ - **Type**:
122
+
123
+ ```ts
124
+ type CustomCollectionSettingsConfig =
125
+ | CustomCollectionSettings
126
+ | Record<string, CustomCollectionSettings>;
127
+ ```
128
+
129
+ - **Default**: `undefined`
130
+
131
+ Allows you to override the Typesense schema configuration. You can pass a single global configuration, or a map of configurations keyed by language (useful if you need specific token separators for languages like Chinese or Japanese).
132
+
133
+ #### Customizing schema and injecting custom data
134
+
135
+ For advanced use cases like adding custom tags for faceted search or boosting the search ranking of specific pages, you can extend the default schema and mutate records before they are indexed.
136
+
137
+ To do this:
138
+
139
+ 1. Use the `getDefaultCollectionFields` helper in `customCollectionSettings` to safely append new fields to the collection schema.
140
+ 2. Use the `transformRecord` hook to populate those fields or modify existing weights based on the `route`.
141
+
142
+ Here is an example showing how to add a custom `category` field for filtering, and how to boost the `page_rank` of "Getting Started" guides:
143
+
144
+ ```ts
145
+ import {
146
+ pluginTypesense,
147
+ getDefaultCollectionFields,
148
+ } from 'rspress-plugin-typesense';
149
+
150
+ pluginTypesense({
151
+ collectionName: 'my_docs',
152
+ serverConfig: {
153
+ /* ... */
154
+ },
155
+
156
+ // 1. Extend the schema to add a custom 'category' field
157
+ customCollectionSettings: {
158
+ en: {
159
+ fields: (params) => [
160
+ ...getDefaultCollectionFields(params),
161
+ { name: 'category', type: 'string', facet: true, optional: true },
162
+ ],
163
+ },
164
+ },
165
+
166
+ // 2. Mutate the record before it gets indexed
167
+ transformRecord(record, route) {
168
+ // Example: Inject a custom tag for faceted search
169
+ if (route.routePath.startsWith('/api/')) {
170
+ record.category = 'API Reference';
171
+ }
172
+
173
+ // Example: Boost the search priority of important pages
174
+ if (route.routePath.includes('getting-started')) {
175
+ record.weight.page_rank = 100; // Default is 0
176
+ }
177
+
178
+ return record;
179
+ },
180
+ });
181
+ ```
182
+
183
+ In the frontend, you could now pass `typesenseSearchParams: { filter_by: 'category:=API Reference' }` to your `<Search />` component to restrict results.
184
+
185
+ ### indexCodeBlocks
186
+
187
+ - **Type**: `boolean`
188
+ - **Default**: `false`
189
+
190
+ By default, the plugin only indexes headers (`h1-h6`), paragraphs, lists and tables. Enabling this will also extract text from inside code blocks.
191
+
192
+ ### versionedSearch
193
+
194
+ - **Type**: `boolean`
195
+ - **Default**: `true`
196
+
197
+ If your Rspress site utilizes multiple versions, the plugin tags every indexed document with its respective version and exposes this setting to the frontend via a virtual module. This ensures users only see search results relevant to the documentation version they are currently viewing. Set this to `false` if you want to search across all versions.
198
+
199
+ ## Search component props (Frontend)
200
+
201
+ The `<PluginTypesenseSearch />` component accepts the following properties:
202
+
203
+ ```ts
204
+ type SearchProps = {
205
+ docSearchProps: TypesenseDocSearchProps;
206
+ locales?: Locales;
207
+ };
208
+ ```
209
+
210
+ ### `docSearchProps`
211
+
212
+ - **Type**: `TypesenseDocSearchProps`
213
+ - **Required**: Yes (`typesenseServerConfig` must be provided)
214
+
215
+ Parameters passed directly to the underlying `typesense-docsearch-react` modal.
216
+
217
+ _You do not need to provide `typesenseCollectionName`. The plugin automatically injects the collection name via a virtual module._
218
+
219
+ ### `locales`
220
+
221
+ - **Type**:
222
+
223
+ ```ts
224
+ type Locales = Record<
225
+ string,
226
+ { translations: DocSearchProps['translations']; placeholder: string }
227
+ >;
228
+ ```
229
+
230
+ - **Default**: `{}`
231
+
232
+ Allows you to customize the placeholder and modal translations based on the active language. You can see the list of translations provided by the plugin [here](/src/runtime/locales.ts).
233
+
234
+ **Example:**
235
+
236
+ ```tsx
237
+ import { Search as PluginTypesenseSearch } from 'rspress-plugin-typesense/runtime';
238
+
239
+ <PluginTypesenseSearch
240
+ locales={{
241
+ en: {
242
+ placeholder: 'Search documentation',
243
+ translations: {
244
+ button: {
245
+ buttonText: 'Search',
246
+ buttonAriaLabel: 'Search',
247
+ },
248
+ },
249
+ },
250
+ ...ZH_LOCALES,
251
+ }}
252
+ />;
253
+ ```
254
+
255
+ ## Local development
256
+
257
+ Install dependencies and watch the library for changes:
258
+
259
+ ```bash
260
+ bun install
261
+ bun run dev
262
+ ```
263
+
264
+ In a second terminal, start the example Rspress site:
265
+
266
+ ```bash
267
+ cd docs
268
+ bun install
269
+ bun run dev
270
+ ```
271
+
272
+ If you want to test indexing and search locally, start Typesense as well:
273
+
274
+ ```bash
275
+ docker compose up -d
276
+ ```
277
+
278
+ ### Integration tests
279
+
280
+ Integration tests use Rstest and require a running Typesense instance. Start Typesense and run the suite:
281
+
282
+ ```bash
283
+ docker compose up -d
284
+ bun run test:integration
285
+ ```
286
+
287
+ The suite builds the package, installs the generated bun tarball into a temporary copy of `docs`, builds the Rspress site, and verifies the results from the Typesense Docker instance.
20
288
 
21
289
  ## License
22
290
 
package/dist/index.d.ts CHANGED
@@ -30,11 +30,6 @@ export interface TypesensePluginOptions {
30
30
  * Defaults to `false` to avoid search noise and bloated index sizes.
31
31
  */
32
32
  indexCodeBlocks?: boolean;
33
- /**
34
- * Whether a failed indexing attempt should crash the build process.
35
- * Defaults to `true`.
36
- */
37
- failOnIndexError?: boolean;
38
33
  /**
39
34
  * Whether to automatically filter search results by the active documentation version.
40
35
  * Defaults to `true`.
package/dist/index.js CHANGED
@@ -72,9 +72,8 @@ function pluginTypesense(options) {
72
72
  } catch (error) {
73
73
  console.error(`\n\x1b[31m✖ [TypesensePlugin] Failed to initialize/create collection:\x1b[0m \x1b[37m${collectionNameTmp}\x1b[0m`);
74
74
  console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m\n`);
75
- if (false !== options.failOnIndexError) throw error;
76
- console.warn("\x1b[33m⚠ [TypesensePlugin] Skipping group indexing due to failOnIndexError=false.\x1b[0m");
77
- continue;
75
+ await helper.discardTmpCollection();
76
+ throw error;
78
77
  }
79
78
  let totalRecords = 0;
80
79
  for (const route of routes){
@@ -95,7 +94,8 @@ function pluginTypesense(options) {
95
94
  const fillerLength = Math.max(2, padLength - route.routePath.length);
96
95
  const filler = '\x1b[90m' + '.'.repeat(fillerLength) + '\x1b[0m';
97
96
  console.warn(` \x1b[33m⚠\x1b[0m \x1b[37m${route.routePath}\x1b[0m ${filler} \x1b[33mskipped\x1b[0m \x1b[90m(HTML not found)\x1b[0m`);
98
- continue;
97
+ await helper.discardTmpCollection();
98
+ throw new Error(`HTML not found for route ${route.routePath}`);
99
99
  }
100
100
  try {
101
101
  const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
@@ -110,9 +110,9 @@ function pluginTypesense(options) {
110
110
  if (error instanceof ImportError) {
111
111
  console.error(" \x1b[31m↳ Import error\x1b[0m");
112
112
  console.error(error.importResults);
113
- if (false !== options.failOnIndexError) throw error;
114
- console.warn("\x1b[33m⚠ [TypesensePlugin] Skipping failure due to failOnIndexError=false.\x1b[0m");
115
113
  } else console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m`);
114
+ await helper.discardTmpCollection();
115
+ throw error;
116
116
  }
117
117
  }
118
118
  try {
@@ -121,8 +121,7 @@ function pluginTypesense(options) {
121
121
  } catch (error) {
122
122
  console.error(`\n\x1b[31m✖ [TypesensePlugin] Failed to commit collection:\x1b[0m \x1b[37m${aliasName}\x1b[0m`);
123
123
  console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m\n`);
124
- if (false !== options.failOnIndexError) throw error;
125
- console.warn("\x1b[33m⚠ [TypesensePlugin] Skipping failure due to failOnIndexError=false.\x1b[0m");
124
+ throw error;
126
125
  }
127
126
  }
128
127
  }
@@ -1,76 +1,9 @@
1
- [class*="DocSearch"] {
2
- --docsearch-searchbox-shadow: inset 0 0 0 2px var(--rp-c-brand);
3
- --docsearch-primary-color: var(--rp-c-brand);
4
- --docsearch-text-color: var(--rp-c-text-1);
5
- --docsearch-secondary-text-color: var(--rp-c-text-2);
6
- --docsearch-muted-color: var(--rp-c-text-2);
7
- --docsearch-subtle-color: var(--rp-c-divider-light);
8
- --docsearch-container-background: #3c3c3c66;
9
- --docsearch-modal-background: var(--rp-c-bg);
10
- --docsearch-search-button-background: color-mix(in srgb,
11
- var(--rp-c-bg) 30%,
12
- transparent);
13
- --docsearch-search-button-text-color: var(--rp-c-text-2);
14
- --docsearch-searchbox-background: color-mix(in srgb,
15
- var(--rp-c-bg) 30%,
16
- transparent);
17
- --docsearch-searchbox-focus-background: color-mix(in srgb,
18
- var(--rp-c-bg) 30%,
19
- transparent);
20
- --docsearch-hit-highlight-color: var(--rp-c-brand-tint);
21
- --docsearch-hit-color: var(--rp-c-text-1);
22
- --docsearch-hit-background: var(--rp-c-bg-soft);
23
- --docsearch-hit-shadow: var(--rp-shadow-2);
24
- --docsearch-footer-background: var(--rp-c-bg);
25
- --docsearch-key-background: var(--rp-c-bg-mute);
26
- --docsearch-key-color: var(--rp-c-text-2);
27
- --docsearch-highlight-color: var(--rp-c-brand);
28
- --docsearch-icon-color: var(--rp-c-text-1);
29
- --docsearch-background-color: var(--rp-c-bg-mute);
30
- --docsearch-focus-color: var(--rp-c-brand);
31
- --docsearch-dropdown-menu-background: var(--rp-c-bg-soft);
32
- --docsearch-dropdown-menu-item-hover-background: var(--rp-c-bg);
33
- }
34
-
35
- html.dark {
36
- --docsearch-text-color: #f5f6f7;
37
- --docsearch-container-background: #090a11cc;
38
- --docsearch-modal-background: #15172a;
39
- --docsearch-modal-shadow: inset 1px 1px 0 0 #2c2e40, 0 3px 8px 0 #000309;
40
- --docsearch-searchbox-background: #090a11;
41
- --docsearch-searchbox-focus-background: #000;
42
- --docsearch-hit-color: #bec3c9;
43
- --docsearch-hit-shadow: none;
44
- --docsearch-hit-background: #090a11;
45
- --docsearch-key-gradient: linear-gradient(-26.5deg,
46
- #565872 0%,
47
- #31355b 100%);
48
- --docsearch-key-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d,
49
- 0 2px 2px 0 #0304094d;
50
- --docsearch-footer-background: #1e2136;
51
- --docsearch-footer-shadow: inset 0 1px 0 0 #494c6a80, 0 -4px 8px 0 #0003;
52
- --docsearch-logo-color: #fff;
53
- --docsearch-muted-color: #7f8497;
54
- }
55
-
56
- .DocSearch {
57
- border-radius: var(--rp-radius-small);
58
- }
59
-
60
- .DocSearch-Button-Container > .DocSearch-Button-Placeholder {
61
- font-size: .825rem;
62
- }
63
-
64
1
  .DocSearch-Button {
65
- border: 1px solid var(--docsearch-subtle-color);
66
- }
67
-
68
- .DocSearch-Button:hover {
69
- box-shadow: none;
70
- color: var(--docsearch-search-button-text-color);
71
- }
72
-
73
- .DocSearch-Screen-Icon svg {
74
- margin: auto;
2
+ color: var(--docsearch-muted-color);
3
+ border-color: var(--rp-c-divider-light, #d1d1d1);
4
+ background-color: var(--docsearch-search-button-background);
5
+ background-color: color-mix(in srgb,
6
+ var(--docsearch-search-button-background) 30%,
7
+ transparent);
75
8
  }
76
9
 
@@ -1,3 +1,3 @@
1
- export { type Locales, RU_LOCALES, ZH_LOCALES, VN_LOCALES } from './locales.js';
1
+ export { type Locales, RU_LOCALES, ZH_LOCALES, VI_LOCALES } from './locales.js';
2
2
  export { Search, type SearchProps } from './Search.js';
3
3
  export type { TypesenseDocSearchProps } from './RealSearch.js';
@@ -1,2 +1,2 @@
1
- export { RU_LOCALES, VN_LOCALES, ZH_LOCALES } from "./locales.js";
1
+ export { RU_LOCALES, VI_LOCALES, ZH_LOCALES } from "./locales.js";
2
2
  export { Search } from "./Search.js";
@@ -1,8 +1,22 @@
1
1
  import type { DocSearchProps } from 'typesense-docsearch-react';
2
- export type Locales = Record<string, {
3
- translations: DocSearchProps['translations'];
2
+ /**
3
+ * Recursively removes `?` and `undefined` from all nested properties
4
+ */
5
+ type DeepRequired<T> = T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? readonly DeepRequired<NonNullable<U>>[] : T extends object ? {
6
+ [K in keyof T]-?: DeepRequired<NonNullable<T[K]>>;
7
+ } : NonNullable<T>;
8
+ export type RequiredTranslations = DeepRequired<NonNullable<DocSearchProps['translations']>>;
9
+ export type StrictLocaleConfig = {
4
10
  placeholder: string;
5
- }>;
6
- export declare const ZH_LOCALES: Locales;
7
- export declare const RU_LOCALES: Locales;
8
- export declare const VN_LOCALES: Locales;
11
+ translations: RequiredTranslations;
12
+ };
13
+ export type LocaleConfig = {
14
+ placeholder: string;
15
+ translations?: DocSearchProps['translations'];
16
+ };
17
+ export type StrictLocales = Record<string, StrictLocaleConfig>;
18
+ export type Locales = Record<string, LocaleConfig>;
19
+ export declare const ZH_LOCALES: StrictLocales;
20
+ export declare const RU_LOCALES: StrictLocales;
21
+ export declare const VI_LOCALES: StrictLocales;
22
+ export {};
@@ -8,10 +8,13 @@ const ZH_LOCALES = {
8
8
  },
9
9
  modal: {
10
10
  searchBox: {
11
- resetButtonTitle: '清除查询条件',
12
- resetButtonAriaLabel: '清除查询条件',
13
- cancelButtonText: '取消',
14
- cancelButtonAriaLabel: '取消'
11
+ clearButtonTitle: '清除查询条件',
12
+ clearButtonAriaLabel: '清除查询条件',
13
+ closeButtonText: '取消',
14
+ closeButtonAriaLabel: '取消',
15
+ placeholderText: '搜索文档',
16
+ enterKeyHint: 'search',
17
+ searchInputLabel: '搜索'
15
18
  },
16
19
  startScreen: {
17
20
  recentSearchesTitle: '搜索历史',
@@ -27,8 +30,15 @@ const ZH_LOCALES = {
27
30
  },
28
31
  footer: {
29
32
  selectText: '选择',
33
+ submitQuestionText: '提交问题',
34
+ selectKeyAriaLabel: '回车键',
30
35
  navigateText: '切换',
36
+ navigateUpKeyAriaLabel: '向上箭头',
37
+ navigateDownKeyAriaLabel: '向下箭头',
31
38
  closeText: '关闭',
39
+ backToSearchText: '返回搜索',
40
+ closeKeyAriaLabel: 'Esc 键',
41
+ poweredByText: '由…提供支持',
32
42
  searchByText: '搜索提供者'
33
43
  },
34
44
  noResultsScreen: {
@@ -36,6 +46,22 @@ const ZH_LOCALES = {
36
46
  suggestedQueryText: '你可以尝试查询',
37
47
  reportMissingResultsText: '你认为该查询应该有结果?',
38
48
  reportMissingResultsLinkText: '点击反馈'
49
+ },
50
+ facets: {
51
+ defaultValueLabel: '全部',
52
+ facetMenuTriggerAriaLabel: '筛选菜单',
53
+ clearAllLabel: '清除全部',
54
+ facetsAriaLabel: '筛选条件',
55
+ selectedFacetsAriaLabel: '已选筛选条件',
56
+ clearFacetAriaLabel: '清除筛选'
57
+ },
58
+ resultsScreen: {
59
+ askAiPlaceholder: '询问 AI:',
60
+ noResultsAskAiPlaceholder: '文档里没找到?让 AI 帮忙:',
61
+ resultsSectionTitle: '搜索结果',
62
+ askAiResultsTitle: 'AI 回答',
63
+ resultBadgeLabelText: '分类',
64
+ recentConversationTimestampFallback: '刚刚'
39
65
  }
40
66
  }
41
67
  }
@@ -51,10 +77,13 @@ const RU_LOCALES = {
51
77
  },
52
78
  modal: {
53
79
  searchBox: {
54
- resetButtonTitle: 'Очистить поиск',
55
- resetButtonAriaLabel: 'Очистить поиск',
56
- cancelButtonText: 'Закрыть',
57
- cancelButtonAriaLabel: 'Закрыть'
80
+ clearButtonTitle: 'Очистить поиск',
81
+ clearButtonAriaLabel: 'Очистить поиск',
82
+ closeButtonText: 'Закрыть',
83
+ closeButtonAriaLabel: 'Закрыть',
84
+ placeholderText: 'Поиск в документации',
85
+ enterKeyHint: 'search',
86
+ searchInputLabel: 'Поиск'
58
87
  },
59
88
  startScreen: {
60
89
  recentSearchesTitle: 'История поиска',
@@ -70,8 +99,15 @@ const RU_LOCALES = {
70
99
  },
71
100
  footer: {
72
101
  selectText: 'выбрать',
102
+ submitQuestionText: 'Задать вопрос',
103
+ selectKeyAriaLabel: 'Клавиша Enter',
73
104
  navigateText: 'перейти',
105
+ navigateUpKeyAriaLabel: 'Стрелка вверх',
106
+ navigateDownKeyAriaLabel: 'Стрелка вниз',
74
107
  closeText: 'закрыть',
108
+ backToSearchText: 'Назад к поиску',
109
+ closeKeyAriaLabel: 'Клавиша Escape',
110
+ poweredByText: 'При поддержке',
75
111
  searchByText: 'поиск от'
76
112
  },
77
113
  noResultsScreen: {
@@ -79,13 +115,29 @@ const RU_LOCALES = {
79
115
  suggestedQueryText: 'Попробуйте изменить запрос',
80
116
  reportMissingResultsText: 'Считаете, что результаты должны быть?',
81
117
  reportMissingResultsLinkText: 'Сообщите об этом'
118
+ },
119
+ facets: {
120
+ defaultValueLabel: 'Все',
121
+ facetMenuTriggerAriaLabel: 'Меню фильтров',
122
+ clearAllLabel: 'Очистить все',
123
+ facetsAriaLabel: 'Фильтры',
124
+ selectedFacetsAriaLabel: 'Выбранные фильтры',
125
+ clearFacetAriaLabel: 'Удалить фильтр'
126
+ },
127
+ resultsScreen: {
128
+ askAiPlaceholder: 'Спросить AI: ',
129
+ noResultsAskAiPlaceholder: 'Не нашли в документации? Спросите AI: ',
130
+ resultsSectionTitle: 'Результаты',
131
+ askAiResultsTitle: 'Ответ AI',
132
+ resultBadgeLabelText: 'Категория',
133
+ recentConversationTimestampFallback: 'Недавно'
82
134
  }
83
135
  }
84
136
  }
85
137
  }
86
138
  };
87
- const VN_LOCALES = {
88
- vn: {
139
+ const VI_LOCALES = {
140
+ vi: {
89
141
  placeholder: 'Tìm kiếm tài liệu',
90
142
  translations: {
91
143
  button: {
@@ -94,10 +146,13 @@ const VN_LOCALES = {
94
146
  },
95
147
  modal: {
96
148
  searchBox: {
97
- resetButtonTitle: 'Xóa truy vấn',
98
- resetButtonAriaLabel: 'Xóa truy vấn',
99
- cancelButtonText: 'Hủy',
100
- cancelButtonAriaLabel: 'Hủy'
149
+ clearButtonTitle: 'Xóa truy vấn',
150
+ clearButtonAriaLabel: 'Xóa truy vấn',
151
+ closeButtonText: 'Hủy',
152
+ closeButtonAriaLabel: 'Hủy',
153
+ placeholderText: 'Tìm kiếm tài liệu',
154
+ enterKeyHint: 'search',
155
+ searchInputLabel: 'Tìm kiếm'
101
156
  },
102
157
  startScreen: {
103
158
  recentSearchesTitle: 'Gần đây',
@@ -113,8 +168,15 @@ const VN_LOCALES = {
113
168
  },
114
169
  footer: {
115
170
  selectText: 'để chọn',
171
+ submitQuestionText: 'Gửi câu hỏi',
172
+ selectKeyAriaLabel: 'Phím Enter',
116
173
  navigateText: 'để di chuyển',
174
+ navigateUpKeyAriaLabel: 'Mũi tên lên',
175
+ navigateDownKeyAriaLabel: 'Mũi tên xuống',
117
176
  closeText: 'để đóng',
177
+ backToSearchText: 'Quay lại tìm kiếm',
178
+ closeKeyAriaLabel: 'Phím Escape',
179
+ poweredByText: 'Vận hành bởi',
118
180
  searchByText: 'Vận hành bởi'
119
181
  },
120
182
  noResultsScreen: {
@@ -122,9 +184,25 @@ const VN_LOCALES = {
122
184
  suggestedQueryText: 'Hãy thử tìm với từ khóa',
123
185
  reportMissingResultsText: 'Bạn nghĩ truy vấn này nên có kết quả?',
124
186
  reportMissingResultsLinkText: 'Hãy cho chúng tôi biết.'
187
+ },
188
+ facets: {
189
+ defaultValueLabel: 'Tất cả',
190
+ facetMenuTriggerAriaLabel: 'Menu bộ lọc',
191
+ clearAllLabel: 'Xóa tất cả',
192
+ facetsAriaLabel: 'Bộ lọc',
193
+ selectedFacetsAriaLabel: 'Các bộ lọc đã chọn',
194
+ clearFacetAriaLabel: 'Xóa bộ lọc'
195
+ },
196
+ resultsScreen: {
197
+ askAiPlaceholder: 'Hỏi AI: ',
198
+ noResultsAskAiPlaceholder: 'Không tìm thấy trong tài liệu? Hỏi AI: ',
199
+ resultsSectionTitle: 'Kết quả tìm kiếm',
200
+ askAiResultsTitle: 'Câu trả lời từ AI',
201
+ resultBadgeLabelText: 'Danh mục',
202
+ recentConversationTimestampFallback: 'Vừa xong'
125
203
  }
126
204
  }
127
205
  }
128
206
  }
129
207
  };
130
- export { RU_LOCALES, VN_LOCALES, ZH_LOCALES };
208
+ export { RU_LOCALES, VI_LOCALES, ZH_LOCALES };
@@ -21,6 +21,7 @@ export declare class TypesenseHelper {
21
21
  init(): Promise<void>;
22
22
  createTmpCollection(): Promise<void>;
23
23
  addRecords(records: DocSearchRecord[], fileName: string, fromSitemap: boolean, padLength?: number): Promise<number>;
24
+ discardTmpCollection(): Promise<void>;
24
25
  commitTmpCollection(): Promise<void>;
25
26
  static transformRecord(record: DocSearchRecord, isVersioned: boolean): any;
26
27
  private getOldCollectionName;
@@ -80,16 +80,42 @@ class TypesenseHelper {
80
80
  console.log(` ${iconColor}✔\x1b[0m \x1b[37m${fileName}\x1b[0m ${filler} \x1b[33m${paddedCount}\x1b[0m \x1b[90mrecords\x1b[0m`);
81
81
  return recordCount;
82
82
  }
83
+ async discardTmpCollection() {
84
+ try {
85
+ await this.typesenseClient.collections(this.collectionNameTmp).delete();
86
+ } catch (error) {
87
+ if (error?.httpStatus !== 404) throw error;
88
+ }
89
+ }
83
90
  async commitTmpCollection() {
84
- const oldCollectionName = await this.getOldCollectionName();
85
- if (oldCollectionName) {
86
- await this.transferSynonyms(oldCollectionName);
87
- await this.transferOverrides(oldCollectionName);
91
+ let oldCollectionName = null;
92
+ let aliasUpdated = false;
93
+ try {
94
+ oldCollectionName = await this.getOldCollectionName();
95
+ if (oldCollectionName) {
96
+ await this.transferSynonyms(oldCollectionName);
97
+ await this.transferOverrides(oldCollectionName);
98
+ }
99
+ await this.typesenseClient.aliases().upsert(this.aliasName, {
100
+ collection_name: this.collectionNameTmp
101
+ });
102
+ aliasUpdated = true;
103
+ if (oldCollectionName) await this.typesenseClient.collections(oldCollectionName).delete();
104
+ } catch (error) {
105
+ try {
106
+ if (aliasUpdated) if (oldCollectionName) await this.typesenseClient.aliases().upsert(this.aliasName, {
107
+ collection_name: oldCollectionName
108
+ });
109
+ else await this.typesenseClient.aliases(this.aliasName).delete();
110
+ await this.discardTmpCollection();
111
+ } catch (rollbackError) {
112
+ const message = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
113
+ throw new Error(`Failed to roll back collection commit for ${this.aliasName}: ${message}`, {
114
+ cause: rollbackError
115
+ });
116
+ }
117
+ throw error;
88
118
  }
89
- await this.typesenseClient.aliases().upsert(this.aliasName, {
90
- collection_name: this.collectionNameTmp
91
- });
92
- if (oldCollectionName) await this.typesenseClient.collections(oldCollectionName).delete();
93
119
  }
94
120
  static transformRecord(record, isVersioned) {
95
121
  const transformedRecord = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rspress-plugin-typesense",
3
3
  "description": "A plugin that adds lightning-fast, typo-tolerant, Typesense-powered search to your Rspress site",
4
- "version": "0.0.2",
4
+ "version": "0.1.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -19,10 +19,13 @@
19
19
  ],
20
20
  "scripts": {
21
21
  "build": "rslib build",
22
- "dev": "rslib build --watch"
22
+ "dev": "rslib build --watch",
23
+ "typecheck": "tsc --noEmit",
24
+ "test:integration": "rstest"
23
25
  },
24
26
  "devDependencies": {
25
27
  "@rslib/core": "^0.20.1",
28
+ "@rstest/core": "0.11.11",
26
29
  "@types/node": "^24.12.0",
27
30
  "rsbuild-plugin-publint": "^0.3.4",
28
31
  "typescript": "^6.0.2"
@@ -32,8 +35,9 @@
32
35
  },
33
36
  "dependencies": {
34
37
  "cheerio": "^1.2.0",
35
- "typesense-docsearch-css": "^0.4.1",
36
- "typesense-docsearch-react": "^3.4.1"
38
+ "typesense": "^3.0.6",
39
+ "typesense-docsearch-css": "5.0.2-0",
40
+ "typesense-docsearch-react": "5.0.2-0"
37
41
  },
38
42
  "keywords": [
39
43
  "typesense",