@gravito/monolith 3.0.0 → 3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravito/monolith",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "Enterprise monolith framework for Gravito Galaxy",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -15,9 +15,12 @@
15
15
  },
16
16
  "scripts": {
17
17
  "build": "tsup src/index.ts --format esm,cjs --dts",
18
- "test": "bun test",
19
- "test:coverage": "bun test --coverage --coverage-threshold=80",
20
- "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
18
+ "test": "bun test --timeout=10000",
19
+ "test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
20
+ "test:ci": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
21
+ "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
22
+ "test:unit": "bun test tests/ --timeout=10000",
23
+ "test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
21
24
  },
22
25
  "keywords": [
23
26
  "gravito",
@@ -28,9 +31,10 @@
28
31
  "author": "Carl Lee <carllee0520@gmail.com>",
29
32
  "license": "MIT",
30
33
  "dependencies": {
31
- "marked": "^11.1.1",
34
+ "@gravito/mass": "workspace:*",
35
+ "@octokit/rest": "^22.0.1",
32
36
  "gray-matter": "^4.0.3",
33
- "@gravito/mass": "workspace:*"
37
+ "marked": "^11.1.1"
34
38
  },
35
39
  "peerDependencies": {
36
40
  "@gravito/core": "workspace:*"
@@ -51,4 +55,4 @@
51
55
  "url": "git+https://github.com/gravito-framework/gravito.git",
52
56
  "directory": "packages/monolith"
53
57
  }
54
- }
58
+ }
@@ -0,0 +1,64 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ const lcovPath = process.argv[2] ?? 'coverage/lcov.info'
5
+ const threshold = Number.parseFloat(process.env.COVERAGE_THRESHOLD ?? '80')
6
+
7
+ const root = resolve(process.cwd())
8
+ const srcRoot = `${resolve(root, 'src')}/`
9
+
10
+ // 檢查 lcov.info 是否存在
11
+ if (!existsSync(lcovPath)) {
12
+ console.error(`Coverage file not found: ${lcovPath}`)
13
+ process.exit(1)
14
+ }
15
+
16
+ let content: string
17
+ try {
18
+ content = readFileSync(lcovPath, 'utf-8')
19
+ } catch (error) {
20
+ console.error(`Failed to read coverage file: ${error}`)
21
+ process.exit(1)
22
+ }
23
+
24
+ const lines = content.split('\n')
25
+
26
+ let currentFile: string | null = null
27
+ let total = 0
28
+ let hit = 0
29
+
30
+ for (const line of lines) {
31
+ if (line.startsWith('SF:')) {
32
+ const filePath = line.slice(3).trim()
33
+ const abs = resolve(root, filePath)
34
+ currentFile = abs.startsWith(srcRoot) ? abs : null
35
+ continue
36
+ }
37
+
38
+ if (!currentFile) {
39
+ continue
40
+ }
41
+
42
+ if (line.startsWith('DA:')) {
43
+ const parts = line.slice(3).split(',')
44
+ if (parts.length >= 2) {
45
+ total += 1
46
+ const count = Number.parseInt(parts[1] ?? '0', 10)
47
+ if (count > 0) {
48
+ hit += 1
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ const percent = total === 0 ? 0 : (hit / total) * 100
55
+ const rounded = Math.round(percent * 100) / 100
56
+
57
+ if (rounded < threshold) {
58
+ console.error(
59
+ `monolith coverage ${rounded}% is below threshold ${threshold}%. Covered lines: ${hit}/${total}.`
60
+ )
61
+ process.exit(1)
62
+ }
63
+
64
+ console.log(`monolith coverage ${rounded}% (${hit}/${total}) meets threshold ${threshold}%.`)
@@ -1,8 +1,12 @@
1
- import { readdir, readFile, stat } from 'node:fs/promises'
2
1
  import { join, parse } from 'node:path'
3
2
  import matter from 'gray-matter'
4
3
  import { marked } from 'marked'
4
+ import type { ContentDriver } from './driver/ContentDriver'
5
5
 
6
+ /**
7
+ * Represents a single content item (file).
8
+ * @public
9
+ */
6
10
  export interface ContentItem {
7
11
  slug: string
8
12
  body: string // The HTML content
@@ -12,14 +16,24 @@ export interface ContentItem {
12
16
  raw: string // The raw markdown
13
17
  }
14
18
 
19
+ /**
20
+ * Configuration for a content collection.
21
+ * @public
22
+ */
15
23
  export interface CollectionConfig {
16
24
  path: string // e.g., 'resources/content/docs'
17
25
  }
18
26
 
27
+ /**
28
+ * Manages fetching, parsing, and caching of filesystem-based content.
29
+ * @public
30
+ */
19
31
  export class ContentManager {
20
32
  private collections = new Map<string, CollectionConfig>()
21
33
  // Simple memory cache: collection:locale:slug -> ContentItem
22
34
  private cache = new Map<string, ContentItem>()
35
+ // In-memory search index: term -> Set<cacheKey>
36
+ private searchIndex = new Map<string, Set<string>>()
23
37
  private renderer = (() => {
24
38
  const renderer = new marked.Renderer()
25
39
  renderer.html = (html: string) => this.escapeHtml(html)
@@ -34,12 +48,41 @@ export class ContentManager {
34
48
  return renderer
35
49
  })()
36
50
 
51
+ /**
52
+ * Clear all cached content.
53
+ * Useful for hot reload during development.
54
+ */
55
+ clearCache(): void {
56
+ this.cache.clear()
57
+ this.searchIndex.clear()
58
+ }
59
+
60
+ /**
61
+ * Invalidate a specific content item.
62
+ * @param collection - The collection name.
63
+ * @param slug - The file slug.
64
+ * @param locale - The locale. Defaults to 'en'.
65
+ */
66
+ invalidate(collection: string, slug: string, locale = 'en'): void {
67
+ const safeSlug = this.sanitizeSegment(slug)
68
+ const safeLocale = this.sanitizeSegment(locale)
69
+
70
+ if (safeSlug && safeLocale) {
71
+ const cacheKey = `${collection}:${safeLocale}:${safeSlug}`
72
+ this.cache.delete(cacheKey)
73
+ }
74
+ }
75
+
76
+ getCollectionConfig(name: string): CollectionConfig | undefined {
77
+ return this.collections.get(name)
78
+ }
79
+
37
80
  /**
38
81
  * Create a new ContentManager instance.
39
82
  *
40
- * @param rootDir - The root directory of the application.
83
+ * @param driver - The content driver to use.
41
84
  */
42
- constructor(private rootDir: string) {}
85
+ constructor(private readonly driver: ContentDriver) {}
43
86
 
44
87
  /**
45
88
  * Register a new content collection.
@@ -73,23 +116,22 @@ export class ContentManager {
73
116
  }
74
117
 
75
118
  const cacheKey = `${collectionName}:${locale}:${slug}`
76
- if (this.cache.has(cacheKey)) {
77
- return this.cache.get(cacheKey)!
119
+ const cachedItem = this.cache.get(cacheKey)
120
+ if (cachedItem) {
121
+ return cachedItem
78
122
  }
79
123
 
80
124
  // Determine path strategy
81
- // Strategy: {root}/{path}/{locale}/{slug}.md
82
- const filePath = join(this.rootDir, config.path, safeLocale, `${safeSlug}.md`)
125
+ // Strategy: {path}/{locale}/{slug}.md
126
+ const filePath = join(config.path, safeLocale, `${safeSlug}.md`)
83
127
 
84
128
  try {
85
- const exists = await stat(filePath)
86
- .then(() => true)
87
- .catch(() => false)
129
+ const exists = await this.driver.exists(filePath)
88
130
  if (!exists) {
89
131
  return null
90
132
  }
91
133
 
92
- const fileContent = await readFile(filePath, 'utf-8')
134
+ const fileContent = await this.driver.read(filePath)
93
135
  const { data, content, excerpt } = matter(fileContent)
94
136
 
95
137
  const html = await marked.parse(content, { renderer: this.renderer })
@@ -103,6 +145,7 @@ export class ContentManager {
103
145
  }
104
146
 
105
147
  this.cache.set(cacheKey, item)
148
+ this.buildSearchIndex(cacheKey, item)
106
149
  return item
107
150
  } catch (e) {
108
151
  console.error(`[Orbit-Content] Error reading file: ${filePath}`, e)
@@ -130,10 +173,10 @@ export class ContentManager {
130
173
  return []
131
174
  }
132
175
 
133
- const dirPath = join(this.rootDir, config.path, safeLocale)
176
+ const dirPath = join(config.path, safeLocale)
134
177
 
135
178
  try {
136
- const files = await readdir(dirPath)
179
+ const files = await this.driver.list(dirPath)
137
180
  const items: ContentItem[] = []
138
181
 
139
182
  for (const file of files) {
@@ -154,6 +197,65 @@ export class ContentManager {
154
197
  }
155
198
  }
156
199
 
200
+ /**
201
+ * Search for content items across collections and locales.
202
+ *
203
+ * @param query - The search query.
204
+ * @param options - Optional filters for collection and locale.
205
+ * @returns An array of matching ContentItems.
206
+ */
207
+ search(query: string, options: { collection?: string; locale?: string } = {}): ContentItem[] {
208
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean)
209
+ if (terms.length === 0) {
210
+ return []
211
+ }
212
+
213
+ const matches = new Set<string>()
214
+
215
+ for (const term of terms) {
216
+ const keys = this.searchIndex.get(term)
217
+ if (keys) {
218
+ for (const key of keys) {
219
+ matches.add(key)
220
+ }
221
+ }
222
+ }
223
+
224
+ const results: ContentItem[] = []
225
+ for (const key of matches) {
226
+ const item = this.cache.get(key)
227
+ if (!item) {
228
+ continue
229
+ }
230
+
231
+ const [collection, locale] = key.split(':')
232
+
233
+ if (options.collection && options.collection !== collection) {
234
+ continue
235
+ }
236
+ if (options.locale && options.locale !== locale) {
237
+ continue
238
+ }
239
+
240
+ results.push(item)
241
+ }
242
+
243
+ return results
244
+ }
245
+
246
+ private buildSearchIndex(cacheKey: string, item: ContentItem): void {
247
+ const text =
248
+ `${item.slug} ${item.meta.title || ''} ${item.raw} ${item.excerpt || ''}`.toLowerCase()
249
+ const terms = text.split(/[^\w\d]+/).filter((t) => t.length > 2)
250
+
251
+ for (const term of terms) {
252
+ if (!this.searchIndex.has(term)) {
253
+ this.searchIndex.set(term, new Set())
254
+ }
255
+ this.searchIndex.get(term)?.add(cacheKey)
256
+ }
257
+ }
258
+
157
259
  private sanitizeSegment(value: string): string | null {
158
260
  if (!value) {
159
261
  return null
@@ -0,0 +1,123 @@
1
+ import { type FSWatcher, readdirSync, watch } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { ContentManager } from './ContentManager'
4
+
5
+ interface WatcherOptions {
6
+ debounceMs?: number
7
+ }
8
+
9
+ export class ContentWatcher {
10
+ private watchers: FSWatcher[] = []
11
+ private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
12
+
13
+ constructor(
14
+ private contentManager: ContentManager,
15
+ private rootDir: string,
16
+ private options: WatcherOptions = {}
17
+ ) {}
18
+
19
+ watch(collectionName: string): void {
20
+ const config = this.contentManager.getCollectionConfig(collectionName)
21
+ if (!config) {
22
+ console.warn(`[ContentWatcher] Collection '${collectionName}' not found`)
23
+ return
24
+ }
25
+
26
+ const watchPath = join(this.rootDir, config.path)
27
+
28
+ // Watch the collection directory (recursive if supported)
29
+ this.addWatcher(watchPath, collectionName)
30
+
31
+ // On some platforms (like Linux), recursive watch is not supported.
32
+ // We manually watch the immediate subdirectories (locales).
33
+ try {
34
+ const entries = readdirSync(watchPath, { withFileTypes: true })
35
+ for (const entry of entries) {
36
+ if (entry.isDirectory()) {
37
+ this.addWatcher(join(watchPath, entry.name), collectionName, entry.name)
38
+ }
39
+ }
40
+ } catch (_e) {
41
+ // Directory might not exist yet
42
+ }
43
+ }
44
+
45
+ private addWatcher(watchPath: string, collection: string, localePrefix?: string) {
46
+ try {
47
+ const watcher = watch(watchPath, { recursive: true }, (_eventType, filename) => {
48
+ if (!filename) {
49
+ return
50
+ }
51
+
52
+ const key = `${collection}:${localePrefix || ''}:${filename}`
53
+ if (this.debounceTimers.has(key)) {
54
+ clearTimeout(this.debounceTimers.get(key))
55
+ }
56
+
57
+ this.debounceTimers.set(
58
+ key,
59
+ setTimeout(() => {
60
+ this.handleFileChange(collection, filename.toString(), localePrefix)
61
+ this.debounceTimers.delete(key)
62
+ }, this.options.debounceMs ?? 100)
63
+ )
64
+ })
65
+
66
+ this.watchers.push(watcher)
67
+ } catch (_e) {
68
+ // If recursive fails, try without it
69
+ try {
70
+ const watcher = watch(watchPath, { recursive: false }, (_eventType, filename) => {
71
+ if (!filename) {
72
+ return
73
+ }
74
+ this.handleFileChange(collection, filename.toString(), localePrefix)
75
+ })
76
+ this.watchers.push(watcher)
77
+ } catch (err) {
78
+ console.error(`[ContentWatcher] Failed to watch ${watchPath}:`, err)
79
+ }
80
+ }
81
+ }
82
+
83
+ private handleFileChange(collection: string, filename: string, localePrefix?: string) {
84
+ // Handle both / and \ as separators
85
+ const parts = filename.split(/[/\\]/)
86
+
87
+ let locale: string
88
+ let file: string
89
+
90
+ if (parts.length >= 2) {
91
+ // If filename is "en/test.md"
92
+ locale = parts[parts.length - 2]
93
+ file = parts[parts.length - 1]
94
+ } else if (localePrefix) {
95
+ // If we are watching a locale directory directly
96
+ locale = localePrefix
97
+ file = parts[0]
98
+ } else {
99
+ // No locale found, and no prefix. Monolith expects collection/locale/slug.md
100
+ return
101
+ }
102
+
103
+ if (!file.endsWith('.md')) {
104
+ return
105
+ }
106
+
107
+ const slug = file.replace(/\.md$/, '')
108
+
109
+ this.contentManager.invalidate(collection, slug, locale)
110
+ }
111
+
112
+ close(): void {
113
+ for (const watcher of this.watchers) {
114
+ watcher.close()
115
+ }
116
+ this.watchers = []
117
+
118
+ for (const timer of this.debounceTimers.values()) {
119
+ clearTimeout(timer)
120
+ }
121
+ this.debounceTimers.clear()
122
+ }
123
+ }
package/src/Controller.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import type { GravitoContext } from '@gravito/core'
2
2
  import { Sanitizer } from './Sanitizer.js'
3
3
 
4
+ /**
5
+ * Base class for all Monolith Controllers.
6
+ *
7
+ * Provides basic functionality for calling actions and sanitizing data.
8
+ *
9
+ * @public
10
+ * @since 3.0.0
11
+ */
4
12
  export abstract class BaseController {
5
13
  protected sanitizer = new Sanitizer()
6
14
 
@@ -14,6 +22,15 @@ export abstract class BaseController {
14
22
  }
15
23
  }
16
24
 
25
+ /**
26
+ * Controller class with request context awareness and helper methods.
27
+ *
28
+ * This class provides a more feature-rich base for controllers that
29
+ * need direct access to the request context and common response helpers.
30
+ *
31
+ * @public
32
+ * @since 3.0.0
33
+ */
17
34
  export abstract class Controller {
18
35
  protected context!: GravitoContext
19
36
 
@@ -2,6 +2,15 @@ import type { GravitoContext, GravitoNext } from '@gravito/core'
2
2
  import { type TSchema, validate } from '@gravito/mass'
3
3
  import { Sanitizer } from './Sanitizer.js'
4
4
 
5
+ /**
6
+ * Base class for Monolith Form Requests.
7
+ *
8
+ * Provides a structured way to handle request validation and authorization
9
+ * for the Monolith architecture.
10
+ *
11
+ * @public
12
+ * @since 3.0.0
13
+ */
5
14
  export abstract class FormRequest {
6
15
  protected context!: GravitoContext
7
16
 
@@ -65,7 +74,9 @@ export abstract class FormRequest {
65
74
  for (const issue of issues) {
66
75
  const path = Array.isArray(issue.path) ? issue.path.join('.') : issue.path || 'root'
67
76
  const key = path.replace(/^\//, '').replace(/\//g, '.')
68
- if (!errors[key]) errors[key] = []
77
+ if (!errors[key]) {
78
+ errors[key] = []
79
+ }
69
80
  errors[key].push(issue.message || 'Validation failed')
70
81
  }
71
82
 
package/src/Router.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { Hono } from 'hono'
2
2
 
3
+ /**
4
+ * Utility for registering resourceful routes.
5
+ * @public
6
+ */
3
7
  export class RouterHelper {
4
8
  /**
5
9
  * Register standard resource routes for a controller.
@@ -0,0 +1,5 @@
1
+ export interface ContentDriver {
2
+ read(path: string): Promise<string>
3
+ exists(path: string): Promise<boolean>
4
+ list(dir: string): Promise<string[]>
5
+ }
@@ -0,0 +1,80 @@
1
+ import { Octokit } from '@octokit/rest'
2
+ import type { ContentDriver } from './ContentDriver'
3
+
4
+ export interface GitHubDriverOptions {
5
+ owner: string
6
+ repo: string
7
+ ref?: string
8
+ auth?: string
9
+ }
10
+
11
+ export class GitHubDriver implements ContentDriver {
12
+ private octokit: Octokit
13
+ private owner: string
14
+ private repo: string
15
+ private ref?: string
16
+
17
+ constructor(options: GitHubDriverOptions) {
18
+ this.octokit = new Octokit({ auth: options.auth })
19
+ this.owner = options.owner
20
+ this.repo = options.repo
21
+ this.ref = options.ref
22
+ }
23
+
24
+ async read(path: string): Promise<string> {
25
+ try {
26
+ const { data } = await this.octokit.rest.repos.getContent({
27
+ owner: this.owner,
28
+ repo: this.repo,
29
+ path,
30
+ ref: this.ref,
31
+ })
32
+
33
+ if (Array.isArray(data) || !('content' in data)) {
34
+ throw new Error(`Path is not a file: ${path}`)
35
+ }
36
+
37
+ return Buffer.from(data.content, 'base64').toString('utf-8')
38
+ } catch (e: any) {
39
+ if (e.status === 404) {
40
+ throw new Error(`File not found: ${path}`)
41
+ }
42
+ throw e
43
+ }
44
+ }
45
+
46
+ async exists(path: string): Promise<boolean> {
47
+ try {
48
+ // Use HEAD request if possible, or just lightweight get
49
+ // getContents with defaults gets JSON metadata
50
+ await this.octokit.rest.repos.getContent({
51
+ owner: this.owner,
52
+ repo: this.repo,
53
+ path,
54
+ ref: this.ref,
55
+ })
56
+ return true
57
+ } catch {
58
+ return false
59
+ }
60
+ }
61
+
62
+ async list(dir: string): Promise<string[]> {
63
+ try {
64
+ const { data } = await this.octokit.rest.repos.getContent({
65
+ owner: this.owner,
66
+ repo: this.repo,
67
+ path: dir,
68
+ ref: this.ref,
69
+ })
70
+
71
+ if (!Array.isArray(data)) {
72
+ return []
73
+ }
74
+
75
+ return data.map((item) => item.name)
76
+ } catch {
77
+ return []
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,30 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import type { ContentDriver } from './ContentDriver'
4
+
5
+ export class LocalDriver implements ContentDriver {
6
+ constructor(private rootDir: string) {}
7
+
8
+ async read(path: string): Promise<string> {
9
+ const fullPath = join(this.rootDir, path)
10
+ return readFile(fullPath, 'utf-8')
11
+ }
12
+
13
+ async exists(path: string): Promise<boolean> {
14
+ try {
15
+ await stat(join(this.rootDir, path))
16
+ return true
17
+ } catch {
18
+ return false
19
+ }
20
+ }
21
+
22
+ async list(dir: string): Promise<string[]> {
23
+ try {
24
+ const fullPath = join(this.rootDir, dir)
25
+ return await readdir(fullPath)
26
+ } catch {
27
+ return []
28
+ }
29
+ }
30
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { GravitoOrbit, PlanetCore } from '@gravito/core'
2
2
  import { type CollectionConfig, ContentManager } from './ContentManager'
3
+ import { ContentWatcher } from './ContentWatcher'
4
+ import type { ContentDriver } from './driver/ContentDriver'
5
+ import { LocalDriver } from './driver/LocalDriver'
3
6
 
4
7
  declare module '@gravito/core' {
5
8
  interface Variables {
@@ -7,17 +10,38 @@ declare module '@gravito/core' {
7
10
  }
8
11
  }
9
12
 
13
+ /**
14
+ * Configuration for Orbit Monolith (Content Engine).
15
+ * @public
16
+ */
10
17
  export interface ContentConfig {
11
18
  root?: string // Defaults to process.cwd()
19
+ driver?: ContentDriver
12
20
  collections?: Record<string, CollectionConfig>
13
21
  }
14
22
 
23
+ /**
24
+ * Orbit Monolith Service.
25
+ * Provides flat-file CMS capabilities to Gravito applications.
26
+ * @public
27
+ */
15
28
  export class OrbitMonolith implements GravitoOrbit {
16
29
  constructor(private config: ContentConfig = {}) {}
17
30
 
18
31
  install(core: PlanetCore): void {
19
32
  const root = this.config.root || process.cwd()
20
- const manager = new ContentManager(root)
33
+ let driver: ContentDriver
34
+ let isLocal = false
35
+
36
+ if (this.config.driver) {
37
+ driver = this.config.driver
38
+ isLocal = driver instanceof LocalDriver
39
+ } else {
40
+ driver = new LocalDriver(root)
41
+ isLocal = true
42
+ }
43
+
44
+ const manager = new ContentManager(driver)
21
45
 
22
46
  // Register Collections from Config
23
47
  if (this.config.collections) {
@@ -35,6 +59,16 @@ export class OrbitMonolith implements GravitoOrbit {
35
59
  return await next()
36
60
  })
37
61
 
62
+ if (process.env.NODE_ENV === 'development' && isLocal) {
63
+ const watcher = new ContentWatcher(manager, root)
64
+ if (this.config.collections) {
65
+ for (const name of Object.keys(this.config.collections)) {
66
+ watcher.watch(name)
67
+ }
68
+ }
69
+ core.logger.info('Orbit Monolith Hot Reload Active 🔥')
70
+ }
71
+
38
72
  core.logger.info('Orbit Monolith installed ⬛️')
39
73
  }
40
74
  }
@@ -42,5 +76,8 @@ export class OrbitMonolith implements GravitoOrbit {
42
76
  export { Schema } from '@gravito/mass'
43
77
  export * from './ContentManager'
44
78
  export * from './Controller'
79
+ export * from './driver/ContentDriver'
80
+ export * from './driver/GitHubDriver'
81
+ export * from './driver/LocalDriver'
45
82
  export * from './FormRequest'
46
83
  export { RouterHelper as Route } from './Router'