@gravito/monolith 3.0.1 → 3.2.1
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/CHANGELOG.md +22 -0
- package/dist/index.cjs +295 -12
- package/dist/index.d.cts +62 -4
- package/dist/index.d.ts +62 -4
- package/dist/index.js +293 -12
- package/ion/src/index.js +2775 -2559
- package/package.json +13 -10
- package/scripts/check-coverage.ts +64 -0
- package/src/ContentManager.ts +103 -13
- package/src/ContentWatcher.ts +123 -0
- package/src/driver/ContentDriver.ts +5 -0
- package/src/driver/GitHubDriver.ts +80 -0
- package/src/driver/LocalDriver.ts +30 -0
- package/src/index.ts +29 -1
- package/tests/content-cache.test.ts +36 -0
- package/tests/content-search.test.ts +79 -0
- package/tests/content-watcher.test.ts +56 -0
- package/tests/content.test.ts +2 -1
- package/tests/extra.test.ts +2 -1
- package/tests/hot-reload.test.ts +74 -0
- package/tsconfig.json +13 -19
- package/dist/src/index.js +0 -5624
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/ContentManager.ts
|
|
2
|
-
import { readdir, readFile, stat } from "fs/promises";
|
|
3
2
|
import { join, parse } from "path";
|
|
4
3
|
import matter from "gray-matter";
|
|
5
4
|
import { marked } from "marked";
|
|
@@ -7,14 +6,16 @@ var ContentManager = class {
|
|
|
7
6
|
/**
|
|
8
7
|
* Create a new ContentManager instance.
|
|
9
8
|
*
|
|
10
|
-
* @param
|
|
9
|
+
* @param driver - The content driver to use.
|
|
11
10
|
*/
|
|
12
|
-
constructor(
|
|
13
|
-
this.
|
|
11
|
+
constructor(driver) {
|
|
12
|
+
this.driver = driver;
|
|
14
13
|
}
|
|
15
14
|
collections = /* @__PURE__ */ new Map();
|
|
16
15
|
// Simple memory cache: collection:locale:slug -> ContentItem
|
|
17
16
|
cache = /* @__PURE__ */ new Map();
|
|
17
|
+
// In-memory search index: term -> Set<cacheKey>
|
|
18
|
+
searchIndex = /* @__PURE__ */ new Map();
|
|
18
19
|
renderer = (() => {
|
|
19
20
|
const renderer = new marked.Renderer();
|
|
20
21
|
renderer.html = (html) => this.escapeHtml(html);
|
|
@@ -28,6 +29,31 @@ var ContentManager = class {
|
|
|
28
29
|
};
|
|
29
30
|
return renderer;
|
|
30
31
|
})();
|
|
32
|
+
/**
|
|
33
|
+
* Clear all cached content.
|
|
34
|
+
* Useful for hot reload during development.
|
|
35
|
+
*/
|
|
36
|
+
clearCache() {
|
|
37
|
+
this.cache.clear();
|
|
38
|
+
this.searchIndex.clear();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Invalidate a specific content item.
|
|
42
|
+
* @param collection - The collection name.
|
|
43
|
+
* @param slug - The file slug.
|
|
44
|
+
* @param locale - The locale. Defaults to 'en'.
|
|
45
|
+
*/
|
|
46
|
+
invalidate(collection, slug, locale = "en") {
|
|
47
|
+
const safeSlug = this.sanitizeSegment(slug);
|
|
48
|
+
const safeLocale = this.sanitizeSegment(locale);
|
|
49
|
+
if (safeSlug && safeLocale) {
|
|
50
|
+
const cacheKey = `${collection}:${safeLocale}:${safeSlug}`;
|
|
51
|
+
this.cache.delete(cacheKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
getCollectionConfig(name) {
|
|
55
|
+
return this.collections.get(name);
|
|
56
|
+
}
|
|
31
57
|
/**
|
|
32
58
|
* Register a new content collection.
|
|
33
59
|
*
|
|
@@ -57,16 +83,17 @@ var ContentManager = class {
|
|
|
57
83
|
return null;
|
|
58
84
|
}
|
|
59
85
|
const cacheKey = `${collectionName}:${locale}:${slug}`;
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
const cachedItem = this.cache.get(cacheKey);
|
|
87
|
+
if (cachedItem) {
|
|
88
|
+
return cachedItem;
|
|
62
89
|
}
|
|
63
|
-
const filePath = join(
|
|
90
|
+
const filePath = join(config.path, safeLocale, `${safeSlug}.md`);
|
|
64
91
|
try {
|
|
65
|
-
const exists = await
|
|
92
|
+
const exists = await this.driver.exists(filePath);
|
|
66
93
|
if (!exists) {
|
|
67
94
|
return null;
|
|
68
95
|
}
|
|
69
|
-
const fileContent = await
|
|
96
|
+
const fileContent = await this.driver.read(filePath);
|
|
70
97
|
const { data, content, excerpt } = matter(fileContent);
|
|
71
98
|
const html = await marked.parse(content, { renderer: this.renderer });
|
|
72
99
|
const item = {
|
|
@@ -77,6 +104,7 @@ var ContentManager = class {
|
|
|
77
104
|
excerpt
|
|
78
105
|
};
|
|
79
106
|
this.cache.set(cacheKey, item);
|
|
107
|
+
this.buildSearchIndex(cacheKey, item);
|
|
80
108
|
return item;
|
|
81
109
|
} catch (e) {
|
|
82
110
|
console.error(`[Orbit-Content] Error reading file: ${filePath}`, e);
|
|
@@ -101,9 +129,9 @@ var ContentManager = class {
|
|
|
101
129
|
if (!safeLocale) {
|
|
102
130
|
return [];
|
|
103
131
|
}
|
|
104
|
-
const dirPath = join(
|
|
132
|
+
const dirPath = join(config.path, safeLocale);
|
|
105
133
|
try {
|
|
106
|
-
const files = await
|
|
134
|
+
const files = await this.driver.list(dirPath);
|
|
107
135
|
const items = [];
|
|
108
136
|
for (const file of files) {
|
|
109
137
|
if (!file.endsWith(".md")) {
|
|
@@ -120,6 +148,54 @@ var ContentManager = class {
|
|
|
120
148
|
return [];
|
|
121
149
|
}
|
|
122
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Search for content items across collections and locales.
|
|
153
|
+
*
|
|
154
|
+
* @param query - The search query.
|
|
155
|
+
* @param options - Optional filters for collection and locale.
|
|
156
|
+
* @returns An array of matching ContentItems.
|
|
157
|
+
*/
|
|
158
|
+
search(query, options = {}) {
|
|
159
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
160
|
+
if (terms.length === 0) {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
const matches = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const term of terms) {
|
|
165
|
+
const keys = this.searchIndex.get(term);
|
|
166
|
+
if (keys) {
|
|
167
|
+
for (const key of keys) {
|
|
168
|
+
matches.add(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const results = [];
|
|
173
|
+
for (const key of matches) {
|
|
174
|
+
const item = this.cache.get(key);
|
|
175
|
+
if (!item) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const [collection, locale] = key.split(":");
|
|
179
|
+
if (options.collection && options.collection !== collection) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (options.locale && options.locale !== locale) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
results.push(item);
|
|
186
|
+
}
|
|
187
|
+
return results;
|
|
188
|
+
}
|
|
189
|
+
buildSearchIndex(cacheKey, item) {
|
|
190
|
+
const text = `${item.slug} ${item.meta.title || ""} ${item.raw} ${item.excerpt || ""}`.toLowerCase();
|
|
191
|
+
const terms = text.split(/[^\w\d]+/).filter((t) => t.length > 2);
|
|
192
|
+
for (const term of terms) {
|
|
193
|
+
if (!this.searchIndex.has(term)) {
|
|
194
|
+
this.searchIndex.set(term, /* @__PURE__ */ new Set());
|
|
195
|
+
}
|
|
196
|
+
this.searchIndex.get(term)?.add(cacheKey);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
123
199
|
sanitizeSegment(value) {
|
|
124
200
|
if (!value) {
|
|
125
201
|
return null;
|
|
@@ -156,6 +232,128 @@ var ContentManager = class {
|
|
|
156
232
|
}
|
|
157
233
|
};
|
|
158
234
|
|
|
235
|
+
// src/ContentWatcher.ts
|
|
236
|
+
import { readdirSync, watch } from "fs";
|
|
237
|
+
import { join as join2 } from "path";
|
|
238
|
+
var ContentWatcher = class {
|
|
239
|
+
constructor(contentManager, rootDir, options = {}) {
|
|
240
|
+
this.contentManager = contentManager;
|
|
241
|
+
this.rootDir = rootDir;
|
|
242
|
+
this.options = options;
|
|
243
|
+
}
|
|
244
|
+
watchers = [];
|
|
245
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
246
|
+
watch(collectionName) {
|
|
247
|
+
const config = this.contentManager.getCollectionConfig(collectionName);
|
|
248
|
+
if (!config) {
|
|
249
|
+
console.warn(`[ContentWatcher] Collection '${collectionName}' not found`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const watchPath = join2(this.rootDir, config.path);
|
|
253
|
+
this.addWatcher(watchPath, collectionName);
|
|
254
|
+
try {
|
|
255
|
+
const entries = readdirSync(watchPath, { withFileTypes: true });
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (entry.isDirectory()) {
|
|
258
|
+
this.addWatcher(join2(watchPath, entry.name), collectionName, entry.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (_e) {
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
addWatcher(watchPath, collection, localePrefix) {
|
|
265
|
+
try {
|
|
266
|
+
const watcher = watch(watchPath, { recursive: true }, (_eventType, filename) => {
|
|
267
|
+
if (!filename) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const key = `${collection}:${localePrefix || ""}:${filename}`;
|
|
271
|
+
if (this.debounceTimers.has(key)) {
|
|
272
|
+
clearTimeout(this.debounceTimers.get(key));
|
|
273
|
+
}
|
|
274
|
+
this.debounceTimers.set(
|
|
275
|
+
key,
|
|
276
|
+
setTimeout(() => {
|
|
277
|
+
this.handleFileChange(collection, filename.toString(), localePrefix);
|
|
278
|
+
this.debounceTimers.delete(key);
|
|
279
|
+
}, this.options.debounceMs ?? 100)
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
this.watchers.push(watcher);
|
|
283
|
+
} catch (_e) {
|
|
284
|
+
try {
|
|
285
|
+
const watcher = watch(watchPath, { recursive: false }, (_eventType, filename) => {
|
|
286
|
+
if (!filename) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.handleFileChange(collection, filename.toString(), localePrefix);
|
|
290
|
+
});
|
|
291
|
+
this.watchers.push(watcher);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error(`[ContentWatcher] Failed to watch ${watchPath}:`, err);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
handleFileChange(collection, filename, localePrefix) {
|
|
298
|
+
const parts = filename.split(/[/\\]/);
|
|
299
|
+
let locale;
|
|
300
|
+
let file;
|
|
301
|
+
if (parts.length >= 2) {
|
|
302
|
+
locale = parts[parts.length - 2];
|
|
303
|
+
file = parts[parts.length - 1];
|
|
304
|
+
} else if (localePrefix) {
|
|
305
|
+
locale = localePrefix;
|
|
306
|
+
file = parts[0];
|
|
307
|
+
} else {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!file.endsWith(".md")) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const slug = file.replace(/\.md$/, "");
|
|
314
|
+
this.contentManager.invalidate(collection, slug, locale);
|
|
315
|
+
}
|
|
316
|
+
close() {
|
|
317
|
+
for (const watcher of this.watchers) {
|
|
318
|
+
watcher.close();
|
|
319
|
+
}
|
|
320
|
+
this.watchers = [];
|
|
321
|
+
for (const timer of this.debounceTimers.values()) {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
}
|
|
324
|
+
this.debounceTimers.clear();
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// src/driver/LocalDriver.ts
|
|
329
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
330
|
+
import { join as join3 } from "path";
|
|
331
|
+
var LocalDriver = class {
|
|
332
|
+
constructor(rootDir) {
|
|
333
|
+
this.rootDir = rootDir;
|
|
334
|
+
}
|
|
335
|
+
async read(path) {
|
|
336
|
+
const fullPath = join3(this.rootDir, path);
|
|
337
|
+
return readFile(fullPath, "utf-8");
|
|
338
|
+
}
|
|
339
|
+
async exists(path) {
|
|
340
|
+
try {
|
|
341
|
+
await stat(join3(this.rootDir, path));
|
|
342
|
+
return true;
|
|
343
|
+
} catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async list(dir) {
|
|
348
|
+
try {
|
|
349
|
+
const fullPath = join3(this.rootDir, dir);
|
|
350
|
+
return await readdir(fullPath);
|
|
351
|
+
} catch {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
159
357
|
// src/index.ts
|
|
160
358
|
import { Schema } from "@gravito/mass";
|
|
161
359
|
|
|
@@ -255,6 +453,69 @@ var Controller = class {
|
|
|
255
453
|
}
|
|
256
454
|
};
|
|
257
455
|
|
|
456
|
+
// src/driver/GitHubDriver.ts
|
|
457
|
+
import { Octokit } from "@octokit/rest";
|
|
458
|
+
var GitHubDriver = class {
|
|
459
|
+
octokit;
|
|
460
|
+
owner;
|
|
461
|
+
repo;
|
|
462
|
+
ref;
|
|
463
|
+
constructor(options) {
|
|
464
|
+
this.octokit = new Octokit({ auth: options.auth });
|
|
465
|
+
this.owner = options.owner;
|
|
466
|
+
this.repo = options.repo;
|
|
467
|
+
this.ref = options.ref;
|
|
468
|
+
}
|
|
469
|
+
async read(path) {
|
|
470
|
+
try {
|
|
471
|
+
const { data } = await this.octokit.rest.repos.getContent({
|
|
472
|
+
owner: this.owner,
|
|
473
|
+
repo: this.repo,
|
|
474
|
+
path,
|
|
475
|
+
ref: this.ref
|
|
476
|
+
});
|
|
477
|
+
if (Array.isArray(data) || !("content" in data)) {
|
|
478
|
+
throw new Error(`Path is not a file: ${path}`);
|
|
479
|
+
}
|
|
480
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
481
|
+
} catch (e) {
|
|
482
|
+
if (e.status === 404) {
|
|
483
|
+
throw new Error(`File not found: ${path}`);
|
|
484
|
+
}
|
|
485
|
+
throw e;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async exists(path) {
|
|
489
|
+
try {
|
|
490
|
+
await this.octokit.rest.repos.getContent({
|
|
491
|
+
owner: this.owner,
|
|
492
|
+
repo: this.repo,
|
|
493
|
+
path,
|
|
494
|
+
ref: this.ref
|
|
495
|
+
});
|
|
496
|
+
return true;
|
|
497
|
+
} catch {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async list(dir) {
|
|
502
|
+
try {
|
|
503
|
+
const { data } = await this.octokit.rest.repos.getContent({
|
|
504
|
+
owner: this.owner,
|
|
505
|
+
repo: this.repo,
|
|
506
|
+
path: dir,
|
|
507
|
+
ref: this.ref
|
|
508
|
+
});
|
|
509
|
+
if (!Array.isArray(data)) {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
return data.map((item) => item.name);
|
|
513
|
+
} catch {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
258
519
|
// src/FormRequest.ts
|
|
259
520
|
import { validate } from "@gravito/mass";
|
|
260
521
|
var FormRequest = class {
|
|
@@ -362,7 +623,16 @@ var OrbitMonolith = class {
|
|
|
362
623
|
}
|
|
363
624
|
install(core) {
|
|
364
625
|
const root = this.config.root || process.cwd();
|
|
365
|
-
|
|
626
|
+
let driver;
|
|
627
|
+
let isLocal = false;
|
|
628
|
+
if (this.config.driver) {
|
|
629
|
+
driver = this.config.driver;
|
|
630
|
+
isLocal = driver instanceof LocalDriver;
|
|
631
|
+
} else {
|
|
632
|
+
driver = new LocalDriver(root);
|
|
633
|
+
isLocal = true;
|
|
634
|
+
}
|
|
635
|
+
const manager = new ContentManager(driver);
|
|
366
636
|
if (this.config.collections) {
|
|
367
637
|
for (const [name, config] of Object.entries(this.config.collections)) {
|
|
368
638
|
manager.defineCollection(name, config);
|
|
@@ -372,6 +642,15 @@ var OrbitMonolith = class {
|
|
|
372
642
|
c.set("content", manager);
|
|
373
643
|
return await next();
|
|
374
644
|
});
|
|
645
|
+
if (process.env.NODE_ENV === "development" && isLocal) {
|
|
646
|
+
const watcher = new ContentWatcher(manager, root);
|
|
647
|
+
if (this.config.collections) {
|
|
648
|
+
for (const name of Object.keys(this.config.collections)) {
|
|
649
|
+
watcher.watch(name);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
core.logger.info("Orbit Monolith Hot Reload Active \u{1F525}");
|
|
653
|
+
}
|
|
375
654
|
core.logger.info("Orbit Monolith installed \u2B1B\uFE0F");
|
|
376
655
|
}
|
|
377
656
|
};
|
|
@@ -380,6 +659,8 @@ export {
|
|
|
380
659
|
ContentManager,
|
|
381
660
|
Controller,
|
|
382
661
|
FormRequest,
|
|
662
|
+
GitHubDriver,
|
|
663
|
+
LocalDriver,
|
|
383
664
|
OrbitMonolith,
|
|
384
665
|
RouterHelper as Route,
|
|
385
666
|
Schema
|