@gravito/monolith 1.0.0-alpha.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/README.md +77 -0
- package/dist/index.js +5596 -0
- package/dist/src/index.js +5596 -0
- package/package.json +40 -0
- package/src/ContentManager.ts +116 -0
- package/src/index.ts +42 -0
- package/tests/content.test.ts +41 -0
- package/tests/fixtures/docs/en/install.md +13 -0
- package/tests/fixtures/docs/zh/install.md +13 -0
- package/tsconfig.json +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gravito/monolith",
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
|
+
"description": "Content management and Markdown rendering orbit for Gravito",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node",
|
|
9
|
+
"test": "bun test"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"gravito",
|
|
13
|
+
"orbit",
|
|
14
|
+
"content",
|
|
15
|
+
"markdown",
|
|
16
|
+
"cms"
|
|
17
|
+
],
|
|
18
|
+
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"gravito-core": "1.0.0-beta.2"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"marked": "^11.1.1",
|
|
25
|
+
"gray-matter": "^4.0.3"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"bun-types": "latest",
|
|
29
|
+
"@types/marked": "^5.0.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/gravito-framework/gravito#readme",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/gravito-framework/gravito.git",
|
|
38
|
+
"directory": "packages/monolith"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
2
|
+
import { join, parse } from 'node:path'
|
|
3
|
+
import matter from 'gray-matter'
|
|
4
|
+
import { marked } from 'marked'
|
|
5
|
+
|
|
6
|
+
export interface ContentItem {
|
|
7
|
+
slug: string
|
|
8
|
+
body: string // The HTML content
|
|
9
|
+
excerpt?: string
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: Frontmatter can be anything
|
|
11
|
+
meta: Record<string, any> // Frontmatter data
|
|
12
|
+
raw: string // The raw markdown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CollectionConfig {
|
|
16
|
+
path: string // e.g., 'resources/content/docs'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ContentManager {
|
|
20
|
+
private collections = new Map<string, CollectionConfig>()
|
|
21
|
+
// Simple memory cache: collection:locale:slug -> ContentItem
|
|
22
|
+
private cache = new Map<string, ContentItem>()
|
|
23
|
+
|
|
24
|
+
constructor(private rootDir: string) {}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register a new content collection
|
|
28
|
+
*/
|
|
29
|
+
defineCollection(name: string, config: CollectionConfig) {
|
|
30
|
+
this.collections.set(name, config)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find a single content item
|
|
35
|
+
* @param collectionName The collection name (e.g. 'docs')
|
|
36
|
+
* @param slug The file slug (e.g. 'installation')
|
|
37
|
+
* @param locale The locale (e.g. 'en')
|
|
38
|
+
*/
|
|
39
|
+
async find(collectionName: string, slug: string, locale = 'en'): Promise<ContentItem | null> {
|
|
40
|
+
const config = this.collections.get(collectionName)
|
|
41
|
+
if (!config) {
|
|
42
|
+
throw new Error(`Collection '${collectionName}' not defined`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const cacheKey = `${collectionName}:${locale}:${slug}`
|
|
46
|
+
if (this.cache.has(cacheKey)) {
|
|
47
|
+
return this.cache.get(cacheKey)!
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Determine path strategy
|
|
51
|
+
// Strategy: {root}/{path}/{locale}/{slug}.md
|
|
52
|
+
const filePath = join(this.rootDir, config.path, locale, `${slug}.md`)
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const exists = await stat(filePath)
|
|
56
|
+
.then(() => true)
|
|
57
|
+
.catch(() => false)
|
|
58
|
+
if (!exists) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fileContent = await readFile(filePath, 'utf-8')
|
|
63
|
+
const { data, content, exemption } = matter(fileContent)
|
|
64
|
+
|
|
65
|
+
const html = await marked.parse(content)
|
|
66
|
+
|
|
67
|
+
const item: ContentItem = {
|
|
68
|
+
slug,
|
|
69
|
+
body: html,
|
|
70
|
+
meta: data,
|
|
71
|
+
raw: content,
|
|
72
|
+
excerpt: exemption,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.cache.set(cacheKey, item)
|
|
76
|
+
return item
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error(`[Orbit-Content] Error reading file: ${filePath}`, e)
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* List all items in a collection for a locale
|
|
85
|
+
* Useful for generating sitemaps or index pages
|
|
86
|
+
*/
|
|
87
|
+
async list(collectionName: string, locale = 'en'): Promise<ContentItem[]> {
|
|
88
|
+
const config = this.collections.get(collectionName)
|
|
89
|
+
if (!config) {
|
|
90
|
+
throw new Error(`Collection '${collectionName}' not defined`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const dirPath = join(this.rootDir, config.path, locale)
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const files = await readdir(dirPath)
|
|
97
|
+
const items: ContentItem[] = []
|
|
98
|
+
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
if (!file.endsWith('.md')) {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const slug = parse(file).name
|
|
104
|
+
const item = await this.find(collectionName, slug, locale)
|
|
105
|
+
if (item) {
|
|
106
|
+
items.push(item)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return items
|
|
111
|
+
} catch (_e) {
|
|
112
|
+
// Directory likely doesn't exist for this locale
|
|
113
|
+
return []
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { GravitoOrbit, PlanetCore } from 'gravito-core'
|
|
2
|
+
import { type CollectionConfig, ContentManager } from './ContentManager'
|
|
3
|
+
|
|
4
|
+
declare module 'gravito-core' {
|
|
5
|
+
interface Variables {
|
|
6
|
+
content: ContentManager
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ContentConfig {
|
|
11
|
+
root?: string // Defaults to process.cwd()
|
|
12
|
+
collections?: Record<string, CollectionConfig>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class OrbitMonolith implements GravitoOrbit {
|
|
16
|
+
constructor(private config: ContentConfig = {}) {}
|
|
17
|
+
|
|
18
|
+
install(core: PlanetCore): void {
|
|
19
|
+
const root = this.config.root || process.cwd()
|
|
20
|
+
const manager = new ContentManager(root)
|
|
21
|
+
|
|
22
|
+
// Register Collections from Config
|
|
23
|
+
if (this.config.collections) {
|
|
24
|
+
for (const [name, config] of Object.entries(this.config.collections)) {
|
|
25
|
+
manager.defineCollection(name, config)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Register Default 'docs' and 'blog' if folders exist?
|
|
30
|
+
// Let's stick to explicit configuration for now to avoid magic.
|
|
31
|
+
|
|
32
|
+
// Inject into request context
|
|
33
|
+
core.adapter.use('*', async (c, next) => {
|
|
34
|
+
c.set('content', manager)
|
|
35
|
+
await next()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
core.logger.info('Orbit Monolith installed ⬛️')
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export * from './ContentManager'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { ContentManager } from '../src/ContentManager'
|
|
4
|
+
|
|
5
|
+
describe('Orbit Content Manager', () => {
|
|
6
|
+
const rootDir = join(import.meta.dir, 'fixtures')
|
|
7
|
+
const manager = new ContentManager(rootDir)
|
|
8
|
+
|
|
9
|
+
manager.defineCollection('docs', {
|
|
10
|
+
path: 'docs',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('it reads markdown content with frontmatter', async () => {
|
|
14
|
+
const item = await manager.find('docs', 'install', 'en')
|
|
15
|
+
|
|
16
|
+
expect(item).not.toBeNull()
|
|
17
|
+
expect(item?.slug).toBe('install')
|
|
18
|
+
expect(item?.meta.title).toBe('Installation Guide')
|
|
19
|
+
expect(item?.body).toContain('<h1>Installation</h1>')
|
|
20
|
+
expect(item?.body).toContain('<pre><code class="language-bash">bun add @gravito/core')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('it respects locale', async () => {
|
|
24
|
+
const item = await manager.find('docs', 'install', 'zh')
|
|
25
|
+
|
|
26
|
+
expect(item).not.toBeNull()
|
|
27
|
+
expect(item?.meta.title).toBe('安裝指南')
|
|
28
|
+
expect(item?.body).toContain('<h1>安裝</h1>')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('it returns null for missing files', async () => {
|
|
32
|
+
const item = await manager.find('docs', 'missing', 'en')
|
|
33
|
+
expect(item).toBeNull()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('it lists items in collection', async () => {
|
|
37
|
+
const items = await manager.list('docs', 'en')
|
|
38
|
+
expect(items.length).toBeGreaterThan(0)
|
|
39
|
+
expect(items[0].slug).toBe('install')
|
|
40
|
+
})
|
|
41
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"baseUrl": ".",
|
|
6
|
+
"paths": {
|
|
7
|
+
"gravito-core": ["../../packages/core/src/index.ts"],
|
|
8
|
+
"@gravito/*": ["../../packages/*/src/index.ts"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*"
|
|
13
|
+
],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist",
|
|
17
|
+
"**/*.test.ts"
|
|
18
|
+
]
|
|
19
|
+
}
|