@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/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 rootDir - The root directory of the application.
9
+ * @param driver - The content driver to use.
11
10
  */
12
- constructor(rootDir) {
13
- this.rootDir = rootDir;
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
- if (this.cache.has(cacheKey)) {
61
- return this.cache.get(cacheKey);
86
+ const cachedItem = this.cache.get(cacheKey);
87
+ if (cachedItem) {
88
+ return cachedItem;
62
89
  }
63
- const filePath = join(this.rootDir, config.path, safeLocale, `${safeSlug}.md`);
90
+ const filePath = join(config.path, safeLocale, `${safeSlug}.md`);
64
91
  try {
65
- const exists = await stat(filePath).then(() => true).catch(() => false);
92
+ const exists = await this.driver.exists(filePath);
66
93
  if (!exists) {
67
94
  return null;
68
95
  }
69
- const fileContent = await readFile(filePath, "utf-8");
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(this.rootDir, config.path, safeLocale);
132
+ const dirPath = join(config.path, safeLocale);
105
133
  try {
106
- const files = await readdir(dirPath);
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
- const manager = new ContentManager(root);
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