@ubean/content 0.6.0 → 0.6.1-beta.2

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 CHANGED
@@ -170,5 +170,28 @@ interface LiveCollection extends Omit<ContentCollection, 'documents' | 'list' |
170
170
  /** Invalidate the cache, forcing the next fetch to call the loader. */
171
171
  invalidate(): void;
172
172
  }
173
+ /**
174
+ * Define a live collection that fetches content at request time.
175
+ *
176
+ * The loader is called on every request (unless caching is enabled).
177
+ * Entries are converted to `ContentDocument` shape and can be queried
178
+ * using the standard query builder.
179
+ *
180
+ * @param options Collection definition
181
+ * @returns LiveCollection instance
182
+ */
183
+ export declare function defineLiveCollection(options: LiveCollectionOptions): LiveCollection;
184
+ /**
185
+ * Get a registered live collection by name.
186
+ */
187
+ export declare function getLiveCollection(name: string): LiveCollection | undefined;
188
+ /**
189
+ * List all registered live collection names.
190
+ */
191
+ export declare function listLiveCollections(): string[];
192
+ /**
193
+ * Clear all registered live collections (useful for testing).
194
+ */
195
+ export declare function clearLiveCollections(): void;
173
196
  //#endregion
174
197
  export { type ContentBody, type ContentCollection, type ContentDocument, type ContentFieldSchema, type ContentModuleOptions, type ContentNavigationItem, type ContentPageRouteOptions, type ContentQueryBuilder, type ContentSchema, type ContentSearchOptions, type ContentSourceConfig, type ContentSourceScanConfig, type ContentTocItem, type ContentType, type DiscoverContentPageRoutesOptions, type GenerateSearchSectionsOptions, type LiveCollection, type LiveCollectionCacheOptions, type LiveCollectionEntry, type LiveCollectionLoader, type LiveCollectionLoaderParams, type LiveCollectionOptions, type MarkdownNode, type PagefindIndexOptions, type PagefindIndexResult, type ParsedContentMeta, type ResolvedContentSearchConfig, type ScanContentSourcesOptions, type SearchHit, type SearchSection, type SectionQueryOptions, type SectionSearchEngine, type SectionSearchOptions, type UbeanContentOptions, bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, defineCollection, defineContentCollection, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, queryCollectionSearchSections, registerContent, scanContentSources, ubeanContentPlugin };
package/dist/index.js CHANGED
@@ -32,4 +32,235 @@ function discoverContentPageRoutes(rootDir, options = {}) {
32
32
  return extractContentPageRoutes(Object.values(collections).flat(), { includeDrafts });
33
33
  }
34
34
  //#endregion
35
- export { bootstrapContentFromDisk, buildNavigation, configureContentRuntime, createContentCollection, createQueryBuilder, createSectionSearch, defineCollection, defineContentCollection, discoverContentPageRoutes, extractContentPageRoutes, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, generateSearchSections, generateSearchSectionsSnapshot, getCollection, getContentItem, listCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, queryCollectionSearchSections, registerContent, resolveContentSearchConfig, runPagefindIndex, scanContentSources, searchSections, splitDocumentIntoSearchSections, tokenizeText, ubeanContentPlugin };
35
+ //#region src/live.ts
36
+ const liveCollections = /* @__PURE__ */ new Map();
37
+ /**
38
+ * Validate an entry against a schema. Only checks required fields and basic
39
+ * types — does not perform full JSON Schema validation. Invalid entries
40
+ * are logged to console.warn and still returned (lenient validation).
41
+ */
42
+ function validateEntry(entry, schema, collectionName) {
43
+ if (!schema) return entry;
44
+ const doc = {
45
+ _type: "json",
46
+ _extension: "json",
47
+ _dir: "",
48
+ _file: "",
49
+ _draft: false,
50
+ _partial: false,
51
+ _empty: false,
52
+ ...entry
53
+ };
54
+ if (schema.required) {
55
+ 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}"`);
56
+ }
57
+ if (schema.properties) for (const [field, fieldSchema] of Object.entries(schema.properties)) {
58
+ const value = entry[field];
59
+ if (value === void 0) continue;
60
+ const expectedType = fieldSchema.type;
61
+ const actualType = Array.isArray(value) ? "array" : typeof value;
62
+ 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}"`);
63
+ }
64
+ return doc;
65
+ }
66
+ /**
67
+ * Define a live collection that fetches content at request time.
68
+ *
69
+ * The loader is called on every request (unless caching is enabled).
70
+ * Entries are converted to `ContentDocument` shape and can be queried
71
+ * using the standard query builder.
72
+ *
73
+ * @param options Collection definition
74
+ * @returns LiveCollection instance
75
+ */
76
+ function defineLiveCollection(options) {
77
+ const cacheStore = /* @__PURE__ */ new Map();
78
+ const fetchEntries = async (params = {}) => {
79
+ const cacheKey = options.cache?.key ? options.cache.key(params) : JSON.stringify(params);
80
+ const now = Date.now();
81
+ if (options.cache && options.cache.ttl > 0) {
82
+ const cached = cacheStore.get(cacheKey);
83
+ if (cached && cached.expiresAt > now) return cached.data;
84
+ }
85
+ const docs = (await options.loader(params)).map((e) => validateEntry(e, options.schema, options.name));
86
+ if (options.cache && options.cache.ttl > 0) cacheStore.set(cacheKey, {
87
+ data: docs,
88
+ expiresAt: now + options.cache.ttl * 1e3
89
+ });
90
+ return docs;
91
+ };
92
+ const collection = {
93
+ name: options.name,
94
+ source: "live",
95
+ type: "json",
96
+ isLive: true,
97
+ loader: options.loader,
98
+ schema: options.schema,
99
+ cache: options.cache,
100
+ async list(params = {}) {
101
+ return fetchEntries(params);
102
+ },
103
+ async getItem(path) {
104
+ const found = (await fetchEntries({ path })).find((d) => d._path === path);
105
+ if (found) return found;
106
+ return (await fetchEntries({})).find((d) => d._path === path) || null;
107
+ },
108
+ query(params = {}) {
109
+ let docsPromise = null;
110
+ const getDocs = () => {
111
+ if (!docsPromise) docsPromise = fetchEntries(params);
112
+ return docsPromise;
113
+ };
114
+ const deferredBuilder = {
115
+ where(fieldOrQuery, operator, value) {
116
+ pendingWhere.push({
117
+ fieldOrQuery,
118
+ operator,
119
+ value
120
+ });
121
+ return deferredBuilder;
122
+ },
123
+ sort(field, direction = "asc") {
124
+ pendingSort.push({
125
+ field,
126
+ direction
127
+ });
128
+ return deferredBuilder;
129
+ },
130
+ limit(count) {
131
+ limitCount = count;
132
+ return deferredBuilder;
133
+ },
134
+ skip(count) {
135
+ skipCount = count;
136
+ return deferredBuilder;
137
+ },
138
+ only(fields) {
139
+ selectedFields = fields;
140
+ return deferredBuilder;
141
+ },
142
+ without(fields) {
143
+ excludedFields = fields;
144
+ return deferredBuilder;
145
+ },
146
+ async find() {
147
+ return applySelection(applyFilters(await getDocs()));
148
+ },
149
+ async findOne() {
150
+ limitCount = 1;
151
+ return applyFilters(await getDocs())[0] || null;
152
+ },
153
+ async findSurround(path, surroundOptions = {}) {
154
+ const before = surroundOptions.before ?? 1;
155
+ const after = surroundOptions.after ?? 1;
156
+ const filtered = applyFilters(await getDocs());
157
+ const index = filtered.findIndex((d) => d._path === path);
158
+ if (index === -1) return [];
159
+ const start = Math.max(0, index - before);
160
+ const end = Math.min(filtered.length, index + after + 1);
161
+ return applySelection(filtered.slice(start, end).filter((_, i) => i !== before));
162
+ },
163
+ async count() {
164
+ return applyFilters(await getDocs()).length;
165
+ }
166
+ };
167
+ let pendingWhere = [];
168
+ let pendingSort = [];
169
+ let limitCount = null;
170
+ let skipCount = 0;
171
+ let selectedFields = null;
172
+ let excludedFields = null;
173
+ function applyFilters(docs) {
174
+ let result = [...docs];
175
+ 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);
176
+ else {
177
+ const field = w.fieldOrQuery;
178
+ let op = w.operator;
179
+ let val = w.value;
180
+ if (val === void 0 && w.operator !== void 0) {
181
+ op = "==";
182
+ val = w.operator;
183
+ }
184
+ result = result.filter((d) => {
185
+ const dv = getNested(d, field);
186
+ switch (op) {
187
+ case "=":
188
+ case "==":
189
+ case void 0: return dv === val;
190
+ case "!=": return dv !== val;
191
+ case ">": return dv > val;
192
+ case ">=": return dv >= val;
193
+ case "<": return dv < val;
194
+ case "<=": return dv <= val;
195
+ case "contains": return String(dv).includes(val);
196
+ case "in": return Array.isArray(val) && val.includes(dv);
197
+ case "exists": return val ? dv !== void 0 : dv === void 0;
198
+ default: return dv === val;
199
+ }
200
+ });
201
+ }
202
+ if (pendingSort.length === 0) pendingSort = [{
203
+ field: "_path",
204
+ direction: "asc"
205
+ }];
206
+ result.sort((a, b) => {
207
+ for (const { field, direction } of pendingSort) {
208
+ const av = getNested(a, field);
209
+ const bv = getNested(b, field);
210
+ if (av === bv) continue;
211
+ const cmp = av < bv ? -1 : 1;
212
+ return direction === "desc" ? -cmp : cmp;
213
+ }
214
+ return 0;
215
+ });
216
+ if (skipCount > 0) result = result.slice(skipCount);
217
+ if (limitCount !== null) result = result.slice(0, limitCount);
218
+ return result;
219
+ }
220
+ function applySelection(docs) {
221
+ if (!selectedFields && !excludedFields) return docs;
222
+ return docs.map((doc) => {
223
+ const newDoc = { ...doc };
224
+ if (excludedFields) for (const f of excludedFields) delete newDoc[f];
225
+ if (selectedFields) {
226
+ const kept = {};
227
+ for (const f of selectedFields) kept[f] = doc[f];
228
+ kept._id = doc._id;
229
+ kept._path = doc._path;
230
+ return kept;
231
+ }
232
+ return newDoc;
233
+ });
234
+ }
235
+ return deferredBuilder;
236
+ },
237
+ invalidate() {
238
+ cacheStore.clear();
239
+ }
240
+ };
241
+ liveCollections.set(options.name, collection);
242
+ return collection;
243
+ }
244
+ /**
245
+ * Get a registered live collection by name.
246
+ */
247
+ function getLiveCollection(name) {
248
+ return liveCollections.get(name);
249
+ }
250
+ /**
251
+ * List all registered live collection names.
252
+ */
253
+ function listLiveCollections() {
254
+ return Array.from(liveCollections.keys());
255
+ }
256
+ /**
257
+ * Clear all registered live collections (useful for testing).
258
+ */
259
+ function clearLiveCollections() {
260
+ liveCollections.clear();
261
+ }
262
+ function getNested(obj, path) {
263
+ return path.split(".").reduce((o, key) => o?.[key], obj);
264
+ }
265
+ //#endregion
266
+ export { bootstrapContentFromDisk, buildNavigation, clearLiveCollections, configureContentRuntime, createContentCollection, createQueryBuilder, createSectionSearch, defineCollection, defineContentCollection, defineLiveCollection, discoverContentPageRoutes, extractContentPageRoutes, fetchContentNavigation, fetchContentNavigation as fetchNavigation, generateId, generateSearchSections, generateSearchSectionsSnapshot, getCollection, getContentItem, getLiveCollection, listCollections, listLiveCollections, parseContent, parseContentFile, parseFrontmatter, parseMarkdown, queryCollection, queryCollection as queryContent, queryCollectionSearchSections, registerContent, resolveContentSearchConfig, runPagefindIndex, scanContentSources, searchSections, splitDocumentIntoSearchSections, tokenizeText, ubeanContentPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/content",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-beta.2",
4
4
  "description": "File-based content module for ubean (markdown/MDX/YAML/JSON)",
5
5
  "files": [
6
6
  "dist"
@@ -28,7 +28,7 @@
28
28
  }
29
29
  },
30
30
  "dependencies": {
31
- "@ubean/shared": "0.6.0",
31
+ "@ubean/shared": "0.6.1-beta.2",
32
32
  "defu": "6.1.7",
33
33
  "pathe": "^2.0.3",
34
34
  "scule": "^1.3.0"
@@ -36,15 +36,19 @@
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.6.4",
38
38
  "typescript": "npm:typescript-native-bridge@latest",
39
- "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
40
- "vite-plus": "1.0.0",
39
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
40
+ "vite-plus": "1.1.0",
41
41
  "vue": "^3.5.43"
42
42
  },
43
43
  "peerDependencies": {
44
+ "pagefind": "^1.0.0",
44
45
  "vite": "^5.0.0 || ^6.0.0",
45
46
  "vue": "^3.5.0"
46
47
  },
47
48
  "peerDependenciesMeta": {
49
+ "pagefind": {
50
+ "optional": true
51
+ },
48
52
  "vite": {
49
53
  "optional": true
50
54
  },