@vuepress-plume/plugin-search 1.0.0-rc.210 → 1.0.0-rc.212
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.
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
watch,
|
|
26
26
|
} from 'vue'
|
|
27
27
|
import { usePageLang, useRouteLocale, useRouter, withBase } from 'vuepress/client'
|
|
28
|
+
import { createTokenizer } from '../../shared/index.js'
|
|
28
29
|
import { useLocale, useSearchIndex } from '../composables/index.js'
|
|
29
30
|
import { LRUCache } from '../utils/index.js'
|
|
30
31
|
import BackIcon from './icons/BackIcon.vue'
|
|
@@ -62,17 +63,61 @@ const isSearching = ref(false)
|
|
|
62
63
|
|
|
63
64
|
const showSearchSpinner = computed(() => isSearchIndexLoading.value || isSearching.value)
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Visible error message when the search index is missing or fails to load.
|
|
68
|
+
*
|
|
69
|
+
* 搜索索引缺失或加载失败时显示的可见提示。
|
|
70
|
+
*/
|
|
71
|
+
const searchIndexError = ref('')
|
|
72
|
+
|
|
73
|
+
const searchIndexErrorText = computed(() =>
|
|
74
|
+
locale.value.searchIndexErrorText || 'Failed to load the search index.',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create an empty index used as a fallback when the index is missing or fails
|
|
79
|
+
* to load, so searching returns an empty result set instead of throwing.
|
|
80
|
+
*
|
|
81
|
+
* 创建空索引,在索引缺失或加载失败时兜底,使检索返回空结果而不是抛出异常。
|
|
82
|
+
*/
|
|
83
|
+
function createEmptyIndex(): MiniSearch<Result> {
|
|
84
|
+
return new MiniSearch<Result>({
|
|
85
|
+
fields: ['title', 'titles', 'text'],
|
|
86
|
+
storeFields: ['title', 'titles'],
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const searchIndex = computedAsync(async (onCancel) => {
|
|
91
|
+
// 标记本次求值是否已被新一轮加载取代(例如 HMR 更新索引)。
|
|
92
|
+
// 过期任务不得再写入错误状态,否则有效结果旁会残留加载失败提示。
|
|
93
|
+
//
|
|
94
|
+
// Mark this evaluation as stale (e.g. superseded by a newer load during HMR).
|
|
95
|
+
// A stale task must not write the error state, otherwise a load failure message
|
|
96
|
+
// would persist next to valid results.
|
|
97
|
+
let canceled = false
|
|
98
|
+
onCancel(() => {
|
|
99
|
+
canceled = true
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
searchIndexError.value = ''
|
|
103
|
+
|
|
104
|
+
// 使用与 Node 端建索引时完全一致的分词策略,避免两端词元不匹配导致检索失效。
|
|
105
|
+
// Use the same tokenizer as the node build side so query tokens always match the index.
|
|
106
|
+
const tokenize = createTokenizer(lang.value)
|
|
107
|
+
|
|
108
|
+
const loadIndex = searchIndexData.value[routeLocale.value]
|
|
109
|
+
// 当前语言缺少索引文件时回退到空索引,并给出可见提示,避免静默无结果。
|
|
110
|
+
// Fall back to an empty index and show a visible message when the locale has no index.
|
|
111
|
+
if (!loadIndex) {
|
|
112
|
+
if (!canceled)
|
|
113
|
+
searchIndexError.value = searchIndexErrorText.value
|
|
114
|
+
return markRaw(createEmptyIndex())
|
|
70
115
|
}
|
|
71
116
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{
|
|
117
|
+
try {
|
|
118
|
+
const json = (await loadIndex())?.default
|
|
119
|
+
return markRaw(
|
|
120
|
+
MiniSearch.loadJSON<Result>(json, {
|
|
76
121
|
fields: ['title', 'titles', 'text'],
|
|
77
122
|
storeFields: ['title', 'titles'],
|
|
78
123
|
searchOptions: {
|
|
@@ -83,9 +128,15 @@ const searchIndex = computedAsync(async () => {
|
|
|
83
128
|
},
|
|
84
129
|
...options.miniSearch?.searchOptions,
|
|
85
130
|
...options.miniSearch?.options,
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
|
-
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error('[plugin-search] failed to load the search index:', error)
|
|
136
|
+
if (!canceled)
|
|
137
|
+
searchIndexError.value = searchIndexErrorText.value
|
|
138
|
+
return markRaw(createEmptyIndex())
|
|
139
|
+
}
|
|
89
140
|
}, undefined, isSearchIndexLoading)
|
|
90
141
|
|
|
91
142
|
const disableQueryPersistence = computed(() =>
|
|
@@ -447,7 +498,14 @@ function selectedClick(e: MouseEvent, p: SearchResult & Result) {
|
|
|
447
498
|
</a>
|
|
448
499
|
</li>
|
|
449
500
|
<li
|
|
450
|
-
v-if="
|
|
501
|
+
v-if="searchIndexError"
|
|
502
|
+
class="no-results error"
|
|
503
|
+
role="alert"
|
|
504
|
+
>
|
|
505
|
+
{{ searchIndexError }}
|
|
506
|
+
</li>
|
|
507
|
+
<li
|
|
508
|
+
v-else-if="filterText && !results.length && enableNoResults"
|
|
451
509
|
class="no-results"
|
|
452
510
|
>
|
|
453
511
|
{{ locale.noResultsText }} "<strong>{{ filterText }}</strong>"
|
|
@@ -815,4 +873,8 @@ svg {
|
|
|
815
873
|
font-size: 0.9rem;
|
|
816
874
|
text-align: center;
|
|
817
875
|
}
|
|
876
|
+
|
|
877
|
+
.no-results.error {
|
|
878
|
+
color: var(--vp-c-danger-1, #b8272c);
|
|
879
|
+
}
|
|
818
880
|
</style>
|
package/dist/node/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {createTokenizer} from '../shared/index.js'
|
|
1
2
|
import MiniSearch from 'minisearch'
|
|
2
3
|
import pMap from 'p-map'
|
|
3
4
|
import sanitizeHTML from 'sanitize-html'
|
|
@@ -14,13 +15,10 @@ const indexCache = new Map()
|
|
|
14
15
|
function getIndexByLocale(locale, lang, options) {
|
|
15
16
|
let index = indexByLocales.get(locale)
|
|
16
17
|
if (!index) {
|
|
17
|
-
const segmenter = new Intl.Segmenter(lang, {granularity: 'word'})
|
|
18
18
|
index = new MiniSearch({
|
|
19
19
|
fields: ['title', 'titles', 'text'],
|
|
20
20
|
storeFields: ['title', 'titles'],
|
|
21
|
-
tokenize(
|
|
22
|
-
return Array.from(segmenter.segment(text)).map((s) => s.segment)
|
|
23
|
-
},
|
|
21
|
+
tokenize: createTokenizer(lang),
|
|
24
22
|
...options.miniSearch?.options,
|
|
25
23
|
})
|
|
26
24
|
indexByLocales.set(locale, index)
|
|
@@ -56,7 +54,46 @@ async function prepareSearchIndex({app, isSearchable, searchOptions}) {
|
|
|
56
54
|
`\n[${colors.green('@vuepress-plume/plugin-search')}] prepare search time spent: ${(performance.now() - start).toFixed(2)}ms`,
|
|
57
55
|
)
|
|
58
56
|
}
|
|
59
|
-
|
|
57
|
+
|
|
58
|
+
let indexTaskQueue = Promise.resolve()
|
|
59
|
+
function runExclusive(task) {
|
|
60
|
+
const result = indexTaskQueue.then(task)
|
|
61
|
+
indexTaskQueue = result.then(
|
|
62
|
+
() => {},
|
|
63
|
+
() => {},
|
|
64
|
+
)
|
|
65
|
+
return result
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function prepareSearchIndexInBackground(options) {
|
|
69
|
+
runExclusive(() => prepareSearchIndex(options)).catch((error) => {
|
|
70
|
+
logger.error(
|
|
71
|
+
`${colors.green('[@vuepress-plume/plugin-search]')} failed to prepare search index:`,
|
|
72
|
+
error,
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function onSearchIndexUpdated(app, {page, isSearchable, searchOptions}) {
|
|
78
|
+
if (isSearchable && !isSearchable(page)) return
|
|
79
|
+
await runExclusive(async () => {
|
|
80
|
+
await indexFile(page, searchOptions, isSearchable)
|
|
81
|
+
await writeTemp(app)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function onSearchIndexRemoved(app, {page, isSearchable, searchOptions}) {
|
|
86
|
+
if (isSearchable && !isSearchable(page)) return
|
|
87
|
+
const cacheKey = page.filePathRelative
|
|
88
|
+
if (!cacheKey) return
|
|
89
|
+
await runExclusive(async () => {
|
|
90
|
+
const index = getIndexByLocale(page.pathLocale, page.lang, searchOptions)
|
|
91
|
+
const cache = indexCache.get(cacheKey)
|
|
92
|
+
if (cache?.length) index.removeAll(cache)
|
|
93
|
+
indexCache.delete(cacheKey)
|
|
94
|
+
await writeTemp(app)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
60
97
|
|
|
61
98
|
async function writeTemp(app) {
|
|
62
99
|
const records = []
|
|
@@ -85,39 +122,40 @@ async function indexFile(page, options, isSearchable) {
|
|
|
85
122
|
if (!page.filePath || page.frontmatter?.search === false) return
|
|
86
123
|
if (isSearchable && !isSearchable(page)) return
|
|
87
124
|
const fileId = page.path
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const index = getIndexByLocale(locale, lang, options)
|
|
91
|
-
const cache = getIndexCache(fileId)
|
|
92
|
-
const sections =
|
|
93
|
-
splitPageIntoSections(`<h1><a href="#"><span>${page.frontmatter.title || page.title}</span></a></h1>
|
|
94
|
-
${page.contentRendered}`)
|
|
125
|
+
const index = getIndexByLocale(page.pathLocale, page.lang, options)
|
|
126
|
+
const cache = getIndexCache(page.filePathRelative ?? fileId)
|
|
95
127
|
try {
|
|
96
|
-
if (cache
|
|
128
|
+
if (cache.length) index.removeAll(cache)
|
|
97
129
|
} catch {}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (index.has(id)) {
|
|
103
|
-
if (anchor)
|
|
130
|
+
cache.length = 0
|
|
131
|
+
for (const item of createPageSectionItems(page))
|
|
132
|
+
if (index.has(item.id)) {
|
|
133
|
+
if (item.id !== fileId)
|
|
104
134
|
logger.error(
|
|
105
|
-
`${colors.green('[@vuepress-plume/plugin-search]')} duplicate heading anchor : ${colors.cyan(titles.join(' >> '))} \n at ${colors.cyan(fileId)}`,
|
|
135
|
+
`${colors.green('[@vuepress-plume/plugin-search]')} duplicate heading anchor : ${colors.cyan([...item.titles, item.title].join(' >> '))} \n at ${colors.cyan(fileId)}`,
|
|
106
136
|
)
|
|
107
137
|
else
|
|
108
138
|
logger.error(
|
|
109
139
|
`${colors.green('[@vuepress-plume/plugin-search]')} duplicate page permalink : ${colors.cyan(fileId)}`,
|
|
110
140
|
)
|
|
111
141
|
} else {
|
|
112
|
-
const item = {
|
|
113
|
-
id,
|
|
114
|
-
text,
|
|
115
|
-
title: titles.at(-1),
|
|
116
|
-
titles: titles.slice(0, -1),
|
|
117
|
-
}
|
|
118
142
|
index.add(item)
|
|
119
143
|
cache.push(item)
|
|
120
144
|
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function* createPageSectionItems(page) {
|
|
148
|
+
const html = `<h1><a href="#"><span>${page.frontmatter.title || page.title || page.filePathRelative || ''}</span></a></h1>\n${page.contentRendered}`
|
|
149
|
+
const fileId = page.path
|
|
150
|
+
for (const section of splitPageIntoSections(html)) {
|
|
151
|
+
const {anchor, text, titles} = section
|
|
152
|
+
if (!text && !titles.length) continue
|
|
153
|
+
yield {
|
|
154
|
+
id: anchor ? `${fileId}#${anchor}` : fileId,
|
|
155
|
+
text,
|
|
156
|
+
title: titles.at(-1),
|
|
157
|
+
titles: titles.slice(0, -1),
|
|
158
|
+
}
|
|
121
159
|
}
|
|
122
160
|
}
|
|
123
161
|
|
|
@@ -191,6 +229,7 @@ const SEARCH_LOCALES = [
|
|
|
191
229
|
resetButtonTitle: 'Reset search',
|
|
192
230
|
backButtonTitle: 'Close search',
|
|
193
231
|
noResultsText: 'No results for',
|
|
232
|
+
searchIndexErrorText: 'Failed to load the search index.',
|
|
194
233
|
footer: {
|
|
195
234
|
selectText: 'to select',
|
|
196
235
|
selectKeyAriaLabel: 'enter',
|
|
@@ -209,6 +248,7 @@ const SEARCH_LOCALES = [
|
|
|
209
248
|
resetButtonTitle: '重置搜索',
|
|
210
249
|
backButtonTitle: '关闭',
|
|
211
250
|
noResultsText: '无搜索结果:',
|
|
251
|
+
searchIndexErrorText: '搜索索引加载失败。',
|
|
212
252
|
footer: {
|
|
213
253
|
selectText: '选择',
|
|
214
254
|
selectKeyAriaLabel: '输入',
|
|
@@ -227,6 +267,7 @@ const SEARCH_LOCALES = [
|
|
|
227
267
|
resetButtonTitle: '重設搜尋',
|
|
228
268
|
backButtonTitle: '關閉',
|
|
229
269
|
noResultsText: '無搜尋結果:',
|
|
270
|
+
searchIndexErrorText: '搜尋索引載入失敗。',
|
|
230
271
|
footer: {
|
|
231
272
|
selectText: '選擇',
|
|
232
273
|
selectKeyAriaLabel: '輸入',
|
|
@@ -245,6 +286,7 @@ const SEARCH_LOCALES = [
|
|
|
245
286
|
resetButtonTitle: 'Suche zurücksetzen',
|
|
246
287
|
backButtonTitle: 'Schließen',
|
|
247
288
|
noResultsText: 'Keine Suchergebnisse:',
|
|
289
|
+
searchIndexErrorText: 'Der Suchindex konnte nicht geladen werden.',
|
|
248
290
|
footer: {
|
|
249
291
|
selectText: 'Auswählen',
|
|
250
292
|
selectKeyAriaLabel: 'Eingabe',
|
|
@@ -263,6 +305,7 @@ const SEARCH_LOCALES = [
|
|
|
263
305
|
resetButtonTitle: 'Réinitialiser la recherche',
|
|
264
306
|
backButtonTitle: 'Fermer',
|
|
265
307
|
noResultsText: 'Aucun résultat trouvé :',
|
|
308
|
+
searchIndexErrorText: 'Échec du chargement de l’index de recherche.',
|
|
266
309
|
footer: {
|
|
267
310
|
selectText: 'sélectionner',
|
|
268
311
|
selectKeyAriaLabel: 'Entrée',
|
|
@@ -281,6 +324,7 @@ const SEARCH_LOCALES = [
|
|
|
281
324
|
resetButtonTitle: 'Сбросить поиск',
|
|
282
325
|
backButtonTitle: 'Закрыть',
|
|
283
326
|
noResultsText: 'Нет результатов поиска:',
|
|
327
|
+
searchIndexErrorText: 'Не удалось загрузить поисковый индекс.',
|
|
284
328
|
footer: {
|
|
285
329
|
selectText: 'Выбрать',
|
|
286
330
|
selectKeyAriaLabel: 'Ввод',
|
|
@@ -299,6 +343,7 @@ const SEARCH_LOCALES = [
|
|
|
299
343
|
resetButtonTitle: '検索をリセット',
|
|
300
344
|
backButtonTitle: '閉じる',
|
|
301
345
|
noResultsText: '検索結果がありません:',
|
|
346
|
+
searchIndexErrorText: '検索インデックスの読み込みに失敗しました。',
|
|
302
347
|
footer: {
|
|
303
348
|
selectText: '選択',
|
|
304
349
|
selectKeyAriaLabel: '入力',
|
|
@@ -343,13 +388,39 @@ function searchPlugin({locales = {}, isSearchable, ...searchOptions} = {}) {
|
|
|
343
388
|
})
|
|
344
389
|
else {
|
|
345
390
|
await prepareSearchIndexPlaceholder(app)
|
|
346
|
-
|
|
391
|
+
prepareSearchIndexInBackground({
|
|
347
392
|
app,
|
|
348
393
|
isSearchable,
|
|
349
394
|
searchOptions,
|
|
350
395
|
})
|
|
351
396
|
}
|
|
352
397
|
},
|
|
398
|
+
onPageUpdated: async (app, type, pageNew, pageOld) => {
|
|
399
|
+
if (type === 'delete') {
|
|
400
|
+
if (pageOld?.filePathRelative)
|
|
401
|
+
await onSearchIndexRemoved(app, {
|
|
402
|
+
page: pageOld,
|
|
403
|
+
isSearchable,
|
|
404
|
+
searchOptions,
|
|
405
|
+
})
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
const becameUnsearchable = !!isSearchable && !!pageNew && !isSearchable(pageNew)
|
|
409
|
+
const page = becameUnsearchable ? pageOld : pageNew
|
|
410
|
+
if (!page?.filePathRelative) return
|
|
411
|
+
if (becameUnsearchable)
|
|
412
|
+
await onSearchIndexRemoved(app, {
|
|
413
|
+
page,
|
|
414
|
+
isSearchable,
|
|
415
|
+
searchOptions,
|
|
416
|
+
})
|
|
417
|
+
else
|
|
418
|
+
await onSearchIndexUpdated(app, {
|
|
419
|
+
page,
|
|
420
|
+
isSearchable,
|
|
421
|
+
searchOptions,
|
|
422
|
+
})
|
|
423
|
+
},
|
|
353
424
|
})
|
|
354
425
|
}
|
|
355
426
|
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface SearchLocaleOptions {
|
|
|
16
16
|
backButtonTitle: string
|
|
17
17
|
/** Text shown when no results found / 无搜索结果时显示的文本 */
|
|
18
18
|
noResultsText: string
|
|
19
|
+
/**
|
|
20
|
+
* Text shown when the search index is missing or fails to load.
|
|
21
|
+
*
|
|
22
|
+
* 搜索索引缺失或加载失败时显示的文本。
|
|
23
|
+
*/
|
|
24
|
+
searchIndexErrorText?: string
|
|
19
25
|
/** Footer keyboard shortcut hints / 底部键盘快捷键提示 */
|
|
20
26
|
footer: {
|
|
21
27
|
/** Text for select action / 选择操作的文本 */
|
|
@@ -40,6 +46,26 @@ export interface SearchLocaleOptions {
|
|
|
40
46
|
* 搜索框的语言配置类型。
|
|
41
47
|
*/
|
|
42
48
|
export type SearchBoxLocales = LocaleConfig<SearchLocaleOptions>
|
|
49
|
+
/**
|
|
50
|
+
* Create a tokenizer shared by the node (index build) and client (query) sides.
|
|
51
|
+
*
|
|
52
|
+
* 创建由 Node 端(构建索引)与客户端(查询)共享的分词器。
|
|
53
|
+
*
|
|
54
|
+
* MiniSearch requires the tokenizer used to build the index to be identical to
|
|
55
|
+
* the one used at query time. Defining it in one place guarantees both sides
|
|
56
|
+
* use the same strategy: `Intl.Segmenter` when available, otherwise the same
|
|
57
|
+
* deterministic whitespace fallback.
|
|
58
|
+
*
|
|
59
|
+
* MiniSearch 要求构建索引与查询时使用完全一致的分词器。集中定义可保证两端策略一致:
|
|
60
|
+
* 支持 `Intl.Segmenter` 时使用它分词,否则统一退化为确定性的空白符分词。
|
|
61
|
+
*
|
|
62
|
+
* @param lang - Language code for word segmentation / 用于分词的语言代码
|
|
63
|
+
* @returns Tokenizer that splits a string into tokens / 将字符串切分为词元的函数
|
|
64
|
+
* @example
|
|
65
|
+
* const tokenize = createTokenizer('en')
|
|
66
|
+
* tokenize('Hello world') // ['Hello', ' ', 'world'] with Intl.Segmenter
|
|
67
|
+
*/
|
|
68
|
+
export declare function createTokenizer(lang: string): (text: string) => string[]
|
|
43
69
|
/**
|
|
44
70
|
* Options for the search plugin.
|
|
45
71
|
*
|
package/dist/shared/index.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
function createTokenizer(lang) {
|
|
2
|
+
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
3
|
+
const segmenter = new Intl.Segmenter(lang, {granularity: 'word'})
|
|
4
|
+
return (text) => Array.from(segmenter.segment(text)).map((segment) => segment.segment)
|
|
5
|
+
}
|
|
6
|
+
return (text) => text.split(/\s+/).filter(Boolean)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export {createTokenizer}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vuepress-plume/plugin-search",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.212",
|
|
5
5
|
"description": "The Plugin for VuePress 2 - local search",
|
|
6
6
|
"author": "pengzhanbo <volodymyr@foxmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"vuepress": "2.0.0-rc.31"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@vuepress/helper": "2.0.0-rc.
|
|
38
|
-
"@vueuse/core": "^
|
|
39
|
-
"@vueuse/integrations": "^
|
|
37
|
+
"@vuepress/helper": "2.0.0-rc.135",
|
|
38
|
+
"@vueuse/core": "^15.0.0",
|
|
39
|
+
"@vueuse/integrations": "^15.0.0",
|
|
40
40
|
"chokidar": "5.0.0",
|
|
41
41
|
"focus-trap": "^8.2.2",
|
|
42
42
|
"mark.js": "^8.11.1",
|
|
43
43
|
"minisearch": "^7.2.0",
|
|
44
|
-
"p-map": "^7.0.
|
|
44
|
+
"p-map": "^7.0.8",
|
|
45
45
|
"sanitize-html": "^2.17.7",
|
|
46
|
-
"vue": "^3.5.
|
|
46
|
+
"vue": "^3.5.43"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public",
|