@ubean/content 0.1.13 → 0.2.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/dist/index.d.ts +82 -1
- package/dist/index.js +233 -2
- package/dist/{runtime-CFSiWnSJ.js → runtime-n9EHNy40.js} +1 -1
- package/dist/runtime.js +1 -1
- package/dist/vite.js +3 -2
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
1
|
import ubeanContentPlugin, { UbeanContentOptions } from "./vite.js";
|
|
2
2
|
import { A as ParsedContentMeta, C as ContentNavigationItem, D as ContentTocItem, E as ContentSourceConfig, O as ContentType, S as ContentModuleOptions, T as ContentSchema, _ as parseMarkdown, a as getContentItem, b as ContentDocument, c as queryCollection, d as createContentCollection, f as createQueryBuilder, g as parseFrontmatter, h as parseContent, i as getCollection, k as MarkdownNode, l as registerContent, m as generateId, n as defineCollection, o as listCollections, p as defineContentCollection, r as fetchContentNavigation, s as parseContentFile, t as configureContentRuntime, u as buildNavigation, v as ContentBody, w as ContentQueryBuilder, x as ContentFieldSchema, y as ContentCollection } from "./runtime-DN9pr02v.js";
|
|
3
|
-
|
|
3
|
+
//#region src/live.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A single entry in a live collection. Must include `_id` and `_path`
|
|
6
|
+
* (same shape as `ContentDocument`, but without the file-system metadata).
|
|
7
|
+
*/
|
|
8
|
+
interface LiveCollectionEntry {
|
|
9
|
+
_id: string;
|
|
10
|
+
_path: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parameters passed to the loader function.
|
|
15
|
+
*/
|
|
16
|
+
interface LiveCollectionLoaderParams {
|
|
17
|
+
/** Filter by a specific path (used by `getItem()`). */
|
|
18
|
+
path?: string;
|
|
19
|
+
/** Arbitrary filter params passed by the caller. */
|
|
20
|
+
filter?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Loader function that fetches collection data at request time.
|
|
24
|
+
* Returns an array of entries. Throw to signal an error.
|
|
25
|
+
*/
|
|
26
|
+
type LiveCollectionLoader = (params: LiveCollectionLoaderParams) => Promise<LiveCollectionEntry[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Cache configuration for a live collection.
|
|
29
|
+
*/
|
|
30
|
+
interface LiveCollectionCacheOptions {
|
|
31
|
+
/** Time-to-live in seconds. 0 disables caching. */
|
|
32
|
+
ttl: number;
|
|
33
|
+
/** Optional cache key generator. Defaults to a stringified params. */
|
|
34
|
+
key?: (params: LiveCollectionLoaderParams) => string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Options for defining a live collection.
|
|
38
|
+
*/
|
|
39
|
+
interface LiveCollectionOptions {
|
|
40
|
+
name: string;
|
|
41
|
+
loader: LiveCollectionLoader;
|
|
42
|
+
schema?: ContentSchema;
|
|
43
|
+
cache?: LiveCollectionCacheOptions;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A live collection that fetches data at request time.
|
|
47
|
+
* Extends `ContentCollection` but with async `list()` and `getItem()`.
|
|
48
|
+
*/
|
|
49
|
+
interface LiveCollection extends Omit<ContentCollection, 'documents' | 'list' | 'getItem' | 'query'> {
|
|
50
|
+
isLive: true;
|
|
51
|
+
loader: LiveCollectionLoader;
|
|
52
|
+
schema?: ContentSchema;
|
|
53
|
+
cache?: LiveCollectionCacheOptions;
|
|
54
|
+
list(params?: LiveCollectionLoaderParams): Promise<ContentDocument[]>;
|
|
55
|
+
getItem(path: string): Promise<ContentDocument | null>;
|
|
56
|
+
query(params?: LiveCollectionLoaderParams): ContentQueryBuilder;
|
|
57
|
+
/** Invalidate the cache, forcing the next fetch to call the loader. */
|
|
58
|
+
invalidate(): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Define a live collection that fetches content at request time.
|
|
62
|
+
*
|
|
63
|
+
* The loader is called on every request (unless caching is enabled).
|
|
64
|
+
* Entries are converted to `ContentDocument` shape and can be queried
|
|
65
|
+
* using the standard query builder.
|
|
66
|
+
*
|
|
67
|
+
* @param options Collection definition
|
|
68
|
+
* @returns LiveCollection instance
|
|
69
|
+
*/
|
|
70
|
+
declare function defineLiveCollection(options: LiveCollectionOptions): LiveCollection;
|
|
71
|
+
/**
|
|
72
|
+
* Get a registered live collection by name.
|
|
73
|
+
*/
|
|
74
|
+
declare function getLiveCollection(name: string): LiveCollection | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* List all registered live collection names.
|
|
77
|
+
*/
|
|
78
|
+
declare function listLiveCollections(): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Clear all registered live collections (useful for testing).
|
|
81
|
+
*/
|
|
82
|
+
declare function clearLiveCollections(): void;
|
|
83
|
+
//#endregion
|
|
84
|
+
export { type ContentBody, type ContentCollection, type ContentDocument, type ContentFieldSchema, type ContentModuleOptions, type ContentNavigationItem, type ContentQueryBuilder, type ContentSchema, type ContentSourceConfig, type ContentTocItem, type ContentType, type LiveCollection, type LiveCollectionCacheOptions, type LiveCollectionEntry, type LiveCollectionLoader, type LiveCollectionLoaderParams, type LiveCollectionOptions, type MarkdownNode, type ParsedContentMeta, type UbeanContentOptions, buildNavigation, clearLiveCollections, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, defineLiveCollection, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, getCollection, getContentItem, getLiveCollection, listCollections, listLiveCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, registerContent, ubeanContentPlugin };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,234 @@
|
|
|
1
|
-
import { _ as parseMarkdown, a as getContentItem, c as queryCollection, d as createContentCollection, f as createQueryBuilder, g as parseFrontmatter, h as parseContent, i as getCollection, l as registerContent, m as generateId, n as defineCollection, o as listCollections, p as defineContentCollection, r as fetchContentNavigation, s as parseContentFile, t as configureContentRuntime, u as buildNavigation } from "./runtime-
|
|
1
|
+
import { _ as parseMarkdown, a as getContentItem, c as queryCollection, d as createContentCollection, f as createQueryBuilder, g as parseFrontmatter, h as parseContent, i as getCollection, l as registerContent, m as generateId, n as defineCollection, o as listCollections, p as defineContentCollection, r as fetchContentNavigation, s as parseContentFile, t as configureContentRuntime, u as buildNavigation } from "./runtime-n9EHNy40.js";
|
|
2
2
|
import ubeanContentPlugin from "./vite.js";
|
|
3
|
-
|
|
3
|
+
//#region src/live.ts
|
|
4
|
+
const liveCollections = /* @__PURE__ */ new Map();
|
|
5
|
+
/**
|
|
6
|
+
* Validate an entry against a schema. Only checks required fields and basic
|
|
7
|
+
* types — does not perform full JSON Schema validation. Invalid entries
|
|
8
|
+
* are logged to console.warn and still returned (lenient validation).
|
|
9
|
+
*/
|
|
10
|
+
function validateEntry(entry, schema, collectionName) {
|
|
11
|
+
if (!schema) return entry;
|
|
12
|
+
const doc = {
|
|
13
|
+
_type: "json",
|
|
14
|
+
_extension: "json",
|
|
15
|
+
_dir: "",
|
|
16
|
+
_file: "",
|
|
17
|
+
_draft: false,
|
|
18
|
+
_partial: false,
|
|
19
|
+
_empty: false,
|
|
20
|
+
...entry
|
|
21
|
+
};
|
|
22
|
+
if (schema.required) {
|
|
23
|
+
for (const field of schema.required) if (entry[field] === void 0) console.warn(`[ubean/content] Live collection "${collectionName}": entry "${entry._id}" is missing required field "${field}"`);
|
|
24
|
+
}
|
|
25
|
+
if (schema.properties) for (const [field, fieldSchema] of Object.entries(schema.properties)) {
|
|
26
|
+
const value = entry[field];
|
|
27
|
+
if (value === void 0) continue;
|
|
28
|
+
const expectedType = fieldSchema.type;
|
|
29
|
+
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
30
|
+
if (expectedType && actualType !== expectedType && !(expectedType === "date" && value instanceof Date)) console.warn(`[ubean/content] Live collection "${collectionName}": field "${field}" expected "${expectedType}" but got "${actualType}" in entry "${entry._id}"`);
|
|
31
|
+
}
|
|
32
|
+
return doc;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Define a live collection that fetches content at request time.
|
|
36
|
+
*
|
|
37
|
+
* The loader is called on every request (unless caching is enabled).
|
|
38
|
+
* Entries are converted to `ContentDocument` shape and can be queried
|
|
39
|
+
* using the standard query builder.
|
|
40
|
+
*
|
|
41
|
+
* @param options Collection definition
|
|
42
|
+
* @returns LiveCollection instance
|
|
43
|
+
*/
|
|
44
|
+
function defineLiveCollection(options) {
|
|
45
|
+
const cacheStore = /* @__PURE__ */ new Map();
|
|
46
|
+
const fetchEntries = async (params = {}) => {
|
|
47
|
+
const cacheKey = options.cache?.key ? options.cache.key(params) : JSON.stringify(params);
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (options.cache && options.cache.ttl > 0) {
|
|
50
|
+
const cached = cacheStore.get(cacheKey);
|
|
51
|
+
if (cached && cached.expiresAt > now) return cached.data;
|
|
52
|
+
}
|
|
53
|
+
const docs = (await options.loader(params)).map((e) => validateEntry(e, options.schema, options.name));
|
|
54
|
+
if (options.cache && options.cache.ttl > 0) cacheStore.set(cacheKey, {
|
|
55
|
+
data: docs,
|
|
56
|
+
expiresAt: now + options.cache.ttl * 1e3
|
|
57
|
+
});
|
|
58
|
+
return docs;
|
|
59
|
+
};
|
|
60
|
+
const collection = {
|
|
61
|
+
name: options.name,
|
|
62
|
+
source: "live",
|
|
63
|
+
type: "json",
|
|
64
|
+
isLive: true,
|
|
65
|
+
loader: options.loader,
|
|
66
|
+
schema: options.schema,
|
|
67
|
+
cache: options.cache,
|
|
68
|
+
async list(params = {}) {
|
|
69
|
+
return fetchEntries(params);
|
|
70
|
+
},
|
|
71
|
+
async getItem(path) {
|
|
72
|
+
const found = (await fetchEntries({ path })).find((d) => d._path === path);
|
|
73
|
+
if (found) return found;
|
|
74
|
+
return (await fetchEntries({})).find((d) => d._path === path) || null;
|
|
75
|
+
},
|
|
76
|
+
query(params = {}) {
|
|
77
|
+
let docsPromise = null;
|
|
78
|
+
const getDocs = () => {
|
|
79
|
+
if (!docsPromise) docsPromise = fetchEntries(params);
|
|
80
|
+
return docsPromise;
|
|
81
|
+
};
|
|
82
|
+
const deferredBuilder = {
|
|
83
|
+
where(fieldOrQuery, operator, value) {
|
|
84
|
+
pendingWhere.push({
|
|
85
|
+
fieldOrQuery,
|
|
86
|
+
operator,
|
|
87
|
+
value
|
|
88
|
+
});
|
|
89
|
+
return deferredBuilder;
|
|
90
|
+
},
|
|
91
|
+
sort(field, direction = "asc") {
|
|
92
|
+
pendingSort.push({
|
|
93
|
+
field,
|
|
94
|
+
direction
|
|
95
|
+
});
|
|
96
|
+
return deferredBuilder;
|
|
97
|
+
},
|
|
98
|
+
limit(count) {
|
|
99
|
+
limitCount = count;
|
|
100
|
+
return deferredBuilder;
|
|
101
|
+
},
|
|
102
|
+
skip(count) {
|
|
103
|
+
skipCount = count;
|
|
104
|
+
return deferredBuilder;
|
|
105
|
+
},
|
|
106
|
+
only(fields) {
|
|
107
|
+
selectedFields = fields;
|
|
108
|
+
return deferredBuilder;
|
|
109
|
+
},
|
|
110
|
+
without(fields) {
|
|
111
|
+
excludedFields = fields;
|
|
112
|
+
return deferredBuilder;
|
|
113
|
+
},
|
|
114
|
+
async find() {
|
|
115
|
+
return applySelection(applyFilters(await getDocs()));
|
|
116
|
+
},
|
|
117
|
+
async findOne() {
|
|
118
|
+
limitCount = 1;
|
|
119
|
+
return applyFilters(await getDocs())[0] || null;
|
|
120
|
+
},
|
|
121
|
+
async findSurround(path, surroundOptions = {}) {
|
|
122
|
+
const before = surroundOptions.before ?? 1;
|
|
123
|
+
const after = surroundOptions.after ?? 1;
|
|
124
|
+
const filtered = applyFilters(await getDocs());
|
|
125
|
+
const index = filtered.findIndex((d) => d._path === path);
|
|
126
|
+
if (index === -1) return [];
|
|
127
|
+
const start = Math.max(0, index - before);
|
|
128
|
+
const end = Math.min(filtered.length, index + after + 1);
|
|
129
|
+
return applySelection(filtered.slice(start, end).filter((_, i) => i !== before));
|
|
130
|
+
},
|
|
131
|
+
async count() {
|
|
132
|
+
return applyFilters(await getDocs()).length;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
let pendingWhere = [];
|
|
136
|
+
let pendingSort = [];
|
|
137
|
+
let limitCount = null;
|
|
138
|
+
let skipCount = 0;
|
|
139
|
+
let selectedFields = null;
|
|
140
|
+
let excludedFields = null;
|
|
141
|
+
function applyFilters(docs) {
|
|
142
|
+
let result = [...docs];
|
|
143
|
+
for (const w of pendingWhere) if (typeof w.fieldOrQuery === "object") for (const [key, val] of Object.entries(w.fieldOrQuery)) result = result.filter((d) => getNested(d, key) === val);
|
|
144
|
+
else {
|
|
145
|
+
const field = w.fieldOrQuery;
|
|
146
|
+
let op = w.operator;
|
|
147
|
+
let val = w.value;
|
|
148
|
+
if (val === void 0 && w.operator !== void 0) {
|
|
149
|
+
op = "==";
|
|
150
|
+
val = w.operator;
|
|
151
|
+
}
|
|
152
|
+
result = result.filter((d) => {
|
|
153
|
+
const dv = getNested(d, field);
|
|
154
|
+
switch (op) {
|
|
155
|
+
case "=":
|
|
156
|
+
case "==":
|
|
157
|
+
case void 0: return dv === val;
|
|
158
|
+
case "!=": return dv !== val;
|
|
159
|
+
case ">": return dv > val;
|
|
160
|
+
case ">=": return dv >= val;
|
|
161
|
+
case "<": return dv < val;
|
|
162
|
+
case "<=": return dv <= val;
|
|
163
|
+
case "contains": return String(dv).includes(val);
|
|
164
|
+
case "in": return Array.isArray(val) && val.includes(dv);
|
|
165
|
+
case "exists": return val ? dv !== void 0 : dv === void 0;
|
|
166
|
+
default: return dv === val;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (pendingSort.length === 0) pendingSort = [{
|
|
171
|
+
field: "_path",
|
|
172
|
+
direction: "asc"
|
|
173
|
+
}];
|
|
174
|
+
result.sort((a, b) => {
|
|
175
|
+
for (const { field, direction } of pendingSort) {
|
|
176
|
+
const av = getNested(a, field);
|
|
177
|
+
const bv = getNested(b, field);
|
|
178
|
+
if (av === bv) continue;
|
|
179
|
+
const cmp = av < bv ? -1 : 1;
|
|
180
|
+
return direction === "desc" ? -cmp : cmp;
|
|
181
|
+
}
|
|
182
|
+
return 0;
|
|
183
|
+
});
|
|
184
|
+
if (skipCount > 0) result = result.slice(skipCount);
|
|
185
|
+
if (limitCount !== null) result = result.slice(0, limitCount);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
function applySelection(docs) {
|
|
189
|
+
if (!selectedFields && !excludedFields) return docs;
|
|
190
|
+
return docs.map((doc) => {
|
|
191
|
+
const newDoc = { ...doc };
|
|
192
|
+
if (excludedFields) for (const f of excludedFields) delete newDoc[f];
|
|
193
|
+
if (selectedFields) {
|
|
194
|
+
const kept = {};
|
|
195
|
+
for (const f of selectedFields) kept[f] = doc[f];
|
|
196
|
+
kept._id = doc._id;
|
|
197
|
+
kept._path = doc._path;
|
|
198
|
+
return kept;
|
|
199
|
+
}
|
|
200
|
+
return newDoc;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return deferredBuilder;
|
|
204
|
+
},
|
|
205
|
+
invalidate() {
|
|
206
|
+
cacheStore.clear();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
liveCollections.set(options.name, collection);
|
|
210
|
+
return collection;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get a registered live collection by name.
|
|
214
|
+
*/
|
|
215
|
+
function getLiveCollection(name) {
|
|
216
|
+
return liveCollections.get(name);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* List all registered live collection names.
|
|
220
|
+
*/
|
|
221
|
+
function listLiveCollections() {
|
|
222
|
+
return Array.from(liveCollections.keys());
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Clear all registered live collections (useful for testing).
|
|
226
|
+
*/
|
|
227
|
+
function clearLiveCollections() {
|
|
228
|
+
liveCollections.clear();
|
|
229
|
+
}
|
|
230
|
+
function getNested(obj, path) {
|
|
231
|
+
return path.split(".").reduce((o, key) => o?.[key], obj);
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
export { buildNavigation, clearLiveCollections, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, defineLiveCollection, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, getCollection, getContentItem, getLiveCollection, listCollections, listLiveCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, registerContent, ubeanContentPlugin };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBasename, getDirname, getExtension, getStem, normalizePath, pathToTitle } from "@ubean/
|
|
1
|
+
import { getBasename, getDirname, getExtension, getStem, normalizePath, pathToTitle } from "@ubean/shared";
|
|
2
2
|
import { kebabCase } from "scule";
|
|
3
3
|
//#region src/core.ts
|
|
4
4
|
function generateId(path, extension) {
|
package/dist/runtime.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as parseMarkdown, a as getContentItem, c as queryCollection, d as createContentCollection, f as createQueryBuilder, g as parseFrontmatter, h as parseContent, i as getCollection, l as registerContent, m as generateId, n as defineCollection, o as listCollections, p as defineContentCollection, r as fetchContentNavigation, s as parseContentFile, t as configureContentRuntime, u as buildNavigation } from "./runtime-
|
|
1
|
+
import { _ as parseMarkdown, a as getContentItem, c as queryCollection, d as createContentCollection, f as createQueryBuilder, g as parseFrontmatter, h as parseContent, i as getCollection, l as registerContent, m as generateId, n as defineCollection, o as listCollections, p as defineContentCollection, r as fetchContentNavigation, s as parseContentFile, t as configureContentRuntime, u as buildNavigation } from "./runtime-n9EHNy40.js";
|
|
2
2
|
export { buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, registerContent };
|
package/dist/vite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as registerContent, s as parseContentFile, t as configureContentRuntime } from "./runtime-
|
|
1
|
+
import { l as registerContent, s as parseContentFile, t as configureContentRuntime } from "./runtime-n9EHNy40.js";
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { join, relative, resolve } from "pathe";
|
|
@@ -43,7 +43,8 @@ function ubeanContentPlugin(userOptions = {}) {
|
|
|
43
43
|
for (const file of files) {
|
|
44
44
|
const fullPath = join(contentDir, file);
|
|
45
45
|
try {
|
|
46
|
-
const
|
|
46
|
+
const raw = readFileSync(fullPath, "utf-8");
|
|
47
|
+
const parsed = parseContentFile(raw, file, { type: sourceConfig.type });
|
|
47
48
|
if (sourceConfig.prefix) parsed._path = sourceConfig.prefix + parsed._path;
|
|
48
49
|
documents.push(parsed);
|
|
49
50
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/content",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "File-based content module for ubean (markdown/MDX/YAML/JSON)",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"defu": "6.1.7",
|
|
28
28
|
"pathe": "^2.0.3",
|
|
29
29
|
"scule": "^1.3.0",
|
|
30
|
-
"@ubean/
|
|
30
|
+
"@ubean/shared": "0.2.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^26.
|
|
33
|
+
"@types/node": "^26.2.0",
|
|
34
34
|
"typescript": "7.0.2",
|
|
35
|
-
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.
|
|
36
|
-
"vite-plus": "0.2.
|
|
35
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.9",
|
|
36
|
+
"vite-plus": "0.2.9"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"vite": "^5.0.0 || ^6.0.0"
|