@gravito/monolith 3.2.0 → 3.2.3
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 +15 -0
- package/README.md +28 -4
- package/dist/index.cjs +25 -26
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +29 -26
- package/doc/KNOWLEDGE_MANAGEMENT.md +60 -0
- package/package.json +10 -6
- package/package.json.bak +58 -0
- package/src/ContentManager.ts +31 -23
- package/src/ContentWatcher.ts +6 -7
- package/src/Router.ts +12 -3
- package/src/middleware/TrimStrings.ts +4 -4
- package/tests/content-watcher.test.ts +14 -1
- package/tests/content.test.ts +60 -0
- package/tsconfig.json +1 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @gravito/monolith
|
|
2
2
|
|
|
3
|
+
## 3.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Convert all workspace:\* dependencies to version numbers for npm publishing
|
|
8
|
+
|
|
9
|
+
- Fixed 144 workspace:\* dependencies across 58 packages
|
|
10
|
+
- Ensures all packages work properly when installed from npm
|
|
11
|
+
- Resolves issues with bunx and npm installation of CLI tools
|
|
12
|
+
- All internal dependencies now use explicit version constraints
|
|
13
|
+
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @gravito/core@1.6.1
|
|
16
|
+
- @gravito/mass@3.0.2
|
|
17
|
+
|
|
3
18
|
## 3.1.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -53,10 +53,34 @@ bun add @gravito/monolith
|
|
|
53
53
|
|
|
54
54
|
## ✨ Features
|
|
55
55
|
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
56
|
+
- 🪐 **Galaxy-Ready Content API**: Native integration with PlanetCore to serve file-based content across all Satellites.
|
|
57
|
+
- 📝 **Markdown-to-State**: Transform flat markdown files into rich, type-safe API responses with zero runtime DB overhead.
|
|
58
|
+
- 📂 **Flexible Collections**: Organize the Galaxy's knowledge into intuitive collections and sub-collections.
|
|
59
|
+
- ⚡ **Zero-Config Performance**: Built-in caching and frontmatter parsing for lightning-fast content retrieval.
|
|
60
|
+
- 🏗️ **SSG Integration**: Works seamlessly with `@gravito/prism` for building blazing-fast static sites.
|
|
61
|
+
|
|
62
|
+
## 🌌 Role in Galaxy Architecture
|
|
63
|
+
|
|
64
|
+
In the **Gravito Galaxy Architecture**, Monolith acts as the **Knowledge Core (Content Layer)**.
|
|
65
|
+
|
|
66
|
+
- **Immutable Truth**: Provides a file-based "Single Source of Truth" for documentation, blogs, and marketing content, allowing developers to manage knowledge via Git.
|
|
67
|
+
- **Micro-CMS Interface**: Enables Satellites to fetch static content through a unified Query API, decoupling presentation from raw data storage.
|
|
68
|
+
- **Hybrid Bridge**: Works with `Atlas` to allow Satellites to combine static markdown knowledge with dynamic relational data in a single view.
|
|
69
|
+
|
|
70
|
+
```mermaid
|
|
71
|
+
graph LR
|
|
72
|
+
Git([Git Repo]) --> MD[Markdown Files]
|
|
73
|
+
MD --> Monolith{Monolith Core}
|
|
74
|
+
Monolith -->|Query| Sat[Satellite: Blog]
|
|
75
|
+
Sat -->|Render| User([User UI])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 📚 Documentation
|
|
79
|
+
|
|
80
|
+
Detailed guides and references for the Galaxy Architecture:
|
|
81
|
+
|
|
82
|
+
- [🏗️ **Architecture Overview**](./README.md) — File-based CMS core.
|
|
83
|
+
- [📝 **Knowledge Management**](./doc/KNOWLEDGE_MANAGEMENT.md) — **NEW**: Collections, frontmatter, and SSG integration.
|
|
60
84
|
|
|
61
85
|
## 📚 API
|
|
62
86
|
|
package/dist/index.cjs
CHANGED
|
@@ -44,8 +44,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
44
44
|
|
|
45
45
|
// src/ContentManager.ts
|
|
46
46
|
var import_node_path = require("path");
|
|
47
|
+
var import_core = require("@gravito/core");
|
|
47
48
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
48
|
-
var import_marked = require("marked");
|
|
49
49
|
var ContentManager = class {
|
|
50
50
|
/**
|
|
51
51
|
* Create a new ContentManager instance.
|
|
@@ -60,19 +60,22 @@ var ContentManager = class {
|
|
|
60
60
|
cache = /* @__PURE__ */ new Map();
|
|
61
61
|
// In-memory search index: term -> Set<cacheKey>
|
|
62
62
|
searchIndex = /* @__PURE__ */ new Map();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
// Escape HTML helper
|
|
64
|
+
escapeHtml = (0, import_core.getEscapeHtml)();
|
|
65
|
+
// RuntimeMarkdownAdapter(自動選擇 Bun 原生或 marked fallback)
|
|
66
|
+
mdAdapter = (0, import_core.getMarkdownAdapter)();
|
|
67
|
+
// 完整的 HTML 渲染回調(含 XSS 防護覆寫)
|
|
68
|
+
renderCallbacks = (0, import_core.createHtmlRenderCallbacks)({
|
|
69
|
+
html: (rawHtml) => this.escapeHtml(rawHtml),
|
|
70
|
+
link: (content, opts) => {
|
|
71
|
+
if (!opts.href || !this.isSafeUrl(opts.href)) {
|
|
72
|
+
return content;
|
|
69
73
|
}
|
|
70
|
-
const safeHref = this.escapeHtml(href);
|
|
71
|
-
const titleAttr = title ? ` title="${this.escapeHtml(title)}"` : "";
|
|
72
|
-
return `<a href="${safeHref}"${titleAttr}>${
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
})();
|
|
74
|
+
const safeHref = this.escapeHtml(opts.href);
|
|
75
|
+
const titleAttr = opts.title ? ` title="${this.escapeHtml(opts.title)}"` : "";
|
|
76
|
+
return `<a href="${safeHref}"${titleAttr}>${content}</a>`;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
76
79
|
/**
|
|
77
80
|
* Clear all cached content.
|
|
78
81
|
* Useful for hot reload during development.
|
|
@@ -139,7 +142,7 @@ var ContentManager = class {
|
|
|
139
142
|
}
|
|
140
143
|
const fileContent = await this.driver.read(filePath);
|
|
141
144
|
const { data, content, excerpt } = (0, import_gray_matter.default)(fileContent);
|
|
142
|
-
const html =
|
|
145
|
+
const html = this.mdAdapter.render(content, this.renderCallbacks);
|
|
143
146
|
const item = {
|
|
144
147
|
slug,
|
|
145
148
|
body: html,
|
|
@@ -255,9 +258,6 @@ var ContentManager = class {
|
|
|
255
258
|
}
|
|
256
259
|
return value;
|
|
257
260
|
}
|
|
258
|
-
escapeHtml(value) {
|
|
259
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
260
|
-
}
|
|
261
261
|
isSafeUrl(href) {
|
|
262
262
|
const trimmed = href.trim();
|
|
263
263
|
if (!trimmed) {
|
|
@@ -315,13 +315,12 @@ var ContentWatcher = class {
|
|
|
315
315
|
if (this.debounceTimers.has(key)) {
|
|
316
316
|
clearTimeout(this.debounceTimers.get(key));
|
|
317
317
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
);
|
|
318
|
+
const timer = setTimeout(() => {
|
|
319
|
+
this.handleFileChange(collection, filename.toString(), localePrefix);
|
|
320
|
+
this.debounceTimers.delete(key);
|
|
321
|
+
}, this.options.debounceMs ?? 100);
|
|
322
|
+
timer.unref?.();
|
|
323
|
+
this.debounceTimers.set(key, timer);
|
|
325
324
|
});
|
|
326
325
|
this.watchers.push(watcher);
|
|
327
326
|
} catch (_e) {
|
|
@@ -653,8 +652,8 @@ var RouterHelper = class {
|
|
|
653
652
|
};
|
|
654
653
|
for (const [method, [verb, suffix]] of Object.entries(routes)) {
|
|
655
654
|
if (typeof controller.prototype[method] === "function") {
|
|
656
|
-
;
|
|
657
|
-
app
|
|
655
|
+
const route = app[verb];
|
|
656
|
+
route.call(app, `${p}${suffix}`, controller.call(method));
|
|
658
657
|
}
|
|
659
658
|
}
|
|
660
659
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -2,7 +2,6 @@ import * as _gravito_core from '@gravito/core';
|
|
|
2
2
|
import { GravitoContext, GravitoOrbit, PlanetCore } from '@gravito/core';
|
|
3
3
|
import { TSchema } from '@gravito/mass';
|
|
4
4
|
export { Schema } from '@gravito/mass';
|
|
5
|
-
import { Hono } from 'hono';
|
|
6
5
|
|
|
7
6
|
interface ContentDriver {
|
|
8
7
|
read(path: string): Promise<string>;
|
|
@@ -30,6 +29,11 @@ interface CollectionConfig {
|
|
|
30
29
|
}
|
|
31
30
|
/**
|
|
32
31
|
* Manages fetching, parsing, and caching of filesystem-based content.
|
|
32
|
+
*
|
|
33
|
+
* 使用 RuntimeMarkdownAdapter 進行 Markdown 渲染,在 Bun 環境下
|
|
34
|
+
* 自動使用原生 C++ 解析器(10-100x 更快),並保留 XSS 防護的
|
|
35
|
+
* 自訂渲染回調。
|
|
36
|
+
*
|
|
33
37
|
* @public
|
|
34
38
|
*/
|
|
35
39
|
declare class ContentManager {
|
|
@@ -37,7 +41,9 @@ declare class ContentManager {
|
|
|
37
41
|
private collections;
|
|
38
42
|
private cache;
|
|
39
43
|
private searchIndex;
|
|
40
|
-
private
|
|
44
|
+
private escapeHtml;
|
|
45
|
+
private readonly mdAdapter;
|
|
46
|
+
private readonly renderCallbacks;
|
|
41
47
|
/**
|
|
42
48
|
* Clear all cached content.
|
|
43
49
|
* Useful for hot reload during development.
|
|
@@ -97,7 +103,6 @@ declare class ContentManager {
|
|
|
97
103
|
}): ContentItem[];
|
|
98
104
|
private buildSearchIndex;
|
|
99
105
|
private sanitizeSegment;
|
|
100
|
-
private escapeHtml;
|
|
101
106
|
private isSafeUrl;
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -230,6 +235,15 @@ declare abstract class FormRequest {
|
|
|
230
235
|
static middleware(): any;
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
/**
|
|
239
|
+
* 路由註冊用的應用介面
|
|
240
|
+
*/
|
|
241
|
+
interface RoutableApp {
|
|
242
|
+
get(path: string, handler: unknown): unknown;
|
|
243
|
+
post(path: string, handler: unknown): unknown;
|
|
244
|
+
put(path: string, handler: unknown): unknown;
|
|
245
|
+
delete(path: string, handler: unknown): unknown;
|
|
246
|
+
}
|
|
233
247
|
/**
|
|
234
248
|
* Utility for registering resourceful routes.
|
|
235
249
|
* @public
|
|
@@ -246,7 +260,7 @@ declare class RouterHelper {
|
|
|
246
260
|
* PUT /prefix/:id -> update
|
|
247
261
|
* DELETE /prefix/:id -> destroy
|
|
248
262
|
*/
|
|
249
|
-
static resource(app:
|
|
263
|
+
static resource(app: RoutableApp, prefix: string, controller: any): void;
|
|
250
264
|
}
|
|
251
265
|
|
|
252
266
|
declare module '@gravito/core' {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,6 @@ import * as _gravito_core from '@gravito/core';
|
|
|
2
2
|
import { GravitoContext, GravitoOrbit, PlanetCore } from '@gravito/core';
|
|
3
3
|
import { TSchema } from '@gravito/mass';
|
|
4
4
|
export { Schema } from '@gravito/mass';
|
|
5
|
-
import { Hono } from 'hono';
|
|
6
5
|
|
|
7
6
|
interface ContentDriver {
|
|
8
7
|
read(path: string): Promise<string>;
|
|
@@ -30,6 +29,11 @@ interface CollectionConfig {
|
|
|
30
29
|
}
|
|
31
30
|
/**
|
|
32
31
|
* Manages fetching, parsing, and caching of filesystem-based content.
|
|
32
|
+
*
|
|
33
|
+
* 使用 RuntimeMarkdownAdapter 進行 Markdown 渲染,在 Bun 環境下
|
|
34
|
+
* 自動使用原生 C++ 解析器(10-100x 更快),並保留 XSS 防護的
|
|
35
|
+
* 自訂渲染回調。
|
|
36
|
+
*
|
|
33
37
|
* @public
|
|
34
38
|
*/
|
|
35
39
|
declare class ContentManager {
|
|
@@ -37,7 +41,9 @@ declare class ContentManager {
|
|
|
37
41
|
private collections;
|
|
38
42
|
private cache;
|
|
39
43
|
private searchIndex;
|
|
40
|
-
private
|
|
44
|
+
private escapeHtml;
|
|
45
|
+
private readonly mdAdapter;
|
|
46
|
+
private readonly renderCallbacks;
|
|
41
47
|
/**
|
|
42
48
|
* Clear all cached content.
|
|
43
49
|
* Useful for hot reload during development.
|
|
@@ -97,7 +103,6 @@ declare class ContentManager {
|
|
|
97
103
|
}): ContentItem[];
|
|
98
104
|
private buildSearchIndex;
|
|
99
105
|
private sanitizeSegment;
|
|
100
|
-
private escapeHtml;
|
|
101
106
|
private isSafeUrl;
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -230,6 +235,15 @@ declare abstract class FormRequest {
|
|
|
230
235
|
static middleware(): any;
|
|
231
236
|
}
|
|
232
237
|
|
|
238
|
+
/**
|
|
239
|
+
* 路由註冊用的應用介面
|
|
240
|
+
*/
|
|
241
|
+
interface RoutableApp {
|
|
242
|
+
get(path: string, handler: unknown): unknown;
|
|
243
|
+
post(path: string, handler: unknown): unknown;
|
|
244
|
+
put(path: string, handler: unknown): unknown;
|
|
245
|
+
delete(path: string, handler: unknown): unknown;
|
|
246
|
+
}
|
|
233
247
|
/**
|
|
234
248
|
* Utility for registering resourceful routes.
|
|
235
249
|
* @public
|
|
@@ -246,7 +260,7 @@ declare class RouterHelper {
|
|
|
246
260
|
* PUT /prefix/:id -> update
|
|
247
261
|
* DELETE /prefix/:id -> destroy
|
|
248
262
|
*/
|
|
249
|
-
static resource(app:
|
|
263
|
+
static resource(app: RoutableApp, prefix: string, controller: any): void;
|
|
250
264
|
}
|
|
251
265
|
|
|
252
266
|
declare module '@gravito/core' {
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// src/ContentManager.ts
|
|
2
2
|
import { join, parse } from "path";
|
|
3
|
+
import {
|
|
4
|
+
createHtmlRenderCallbacks,
|
|
5
|
+
getEscapeHtml,
|
|
6
|
+
getMarkdownAdapter
|
|
7
|
+
} from "@gravito/core";
|
|
3
8
|
import matter from "gray-matter";
|
|
4
|
-
import { marked } from "marked";
|
|
5
9
|
var ContentManager = class {
|
|
6
10
|
/**
|
|
7
11
|
* Create a new ContentManager instance.
|
|
@@ -16,19 +20,22 @@ var ContentManager = class {
|
|
|
16
20
|
cache = /* @__PURE__ */ new Map();
|
|
17
21
|
// In-memory search index: term -> Set<cacheKey>
|
|
18
22
|
searchIndex = /* @__PURE__ */ new Map();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Escape HTML helper
|
|
24
|
+
escapeHtml = getEscapeHtml();
|
|
25
|
+
// RuntimeMarkdownAdapter(自動選擇 Bun 原生或 marked fallback)
|
|
26
|
+
mdAdapter = getMarkdownAdapter();
|
|
27
|
+
// 完整的 HTML 渲染回調(含 XSS 防護覆寫)
|
|
28
|
+
renderCallbacks = createHtmlRenderCallbacks({
|
|
29
|
+
html: (rawHtml) => this.escapeHtml(rawHtml),
|
|
30
|
+
link: (content, opts) => {
|
|
31
|
+
if (!opts.href || !this.isSafeUrl(opts.href)) {
|
|
32
|
+
return content;
|
|
25
33
|
}
|
|
26
|
-
const safeHref = this.escapeHtml(href);
|
|
27
|
-
const titleAttr = title ? ` title="${this.escapeHtml(title)}"` : "";
|
|
28
|
-
return `<a href="${safeHref}"${titleAttr}>${
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
})();
|
|
34
|
+
const safeHref = this.escapeHtml(opts.href);
|
|
35
|
+
const titleAttr = opts.title ? ` title="${this.escapeHtml(opts.title)}"` : "";
|
|
36
|
+
return `<a href="${safeHref}"${titleAttr}>${content}</a>`;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
32
39
|
/**
|
|
33
40
|
* Clear all cached content.
|
|
34
41
|
* Useful for hot reload during development.
|
|
@@ -95,7 +102,7 @@ var ContentManager = class {
|
|
|
95
102
|
}
|
|
96
103
|
const fileContent = await this.driver.read(filePath);
|
|
97
104
|
const { data, content, excerpt } = matter(fileContent);
|
|
98
|
-
const html =
|
|
105
|
+
const html = this.mdAdapter.render(content, this.renderCallbacks);
|
|
99
106
|
const item = {
|
|
100
107
|
slug,
|
|
101
108
|
body: html,
|
|
@@ -211,9 +218,6 @@ var ContentManager = class {
|
|
|
211
218
|
}
|
|
212
219
|
return value;
|
|
213
220
|
}
|
|
214
|
-
escapeHtml(value) {
|
|
215
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
216
|
-
}
|
|
217
221
|
isSafeUrl(href) {
|
|
218
222
|
const trimmed = href.trim();
|
|
219
223
|
if (!trimmed) {
|
|
@@ -271,13 +275,12 @@ var ContentWatcher = class {
|
|
|
271
275
|
if (this.debounceTimers.has(key)) {
|
|
272
276
|
clearTimeout(this.debounceTimers.get(key));
|
|
273
277
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
);
|
|
278
|
+
const timer = setTimeout(() => {
|
|
279
|
+
this.handleFileChange(collection, filename.toString(), localePrefix);
|
|
280
|
+
this.debounceTimers.delete(key);
|
|
281
|
+
}, this.options.debounceMs ?? 100);
|
|
282
|
+
timer.unref?.();
|
|
283
|
+
this.debounceTimers.set(key, timer);
|
|
281
284
|
});
|
|
282
285
|
this.watchers.push(watcher);
|
|
283
286
|
} catch (_e) {
|
|
@@ -609,8 +612,8 @@ var RouterHelper = class {
|
|
|
609
612
|
};
|
|
610
613
|
for (const [method, [verb, suffix]] of Object.entries(routes)) {
|
|
611
614
|
if (typeof controller.prototype[method] === "function") {
|
|
612
|
-
;
|
|
613
|
-
app
|
|
615
|
+
const route = app[verb];
|
|
616
|
+
route.call(app, `${p}${suffix}`, controller.call(method));
|
|
614
617
|
}
|
|
615
618
|
}
|
|
616
619
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Knowledge Management Guide
|
|
2
|
+
|
|
3
|
+
`@gravito/monolith` allows you to treat your markdown files as a structured data source, enabling Git-based content management.
|
|
4
|
+
|
|
5
|
+
## 1. Defining Collections
|
|
6
|
+
|
|
7
|
+
Organize your content into logical folders. Each folder becomes a "Collection".
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
content/
|
|
11
|
+
├── docs/ # Collection: 'docs'
|
|
12
|
+
│ ├── install.md
|
|
13
|
+
│ └── api.md
|
|
14
|
+
└── blog/ # Collection: 'blog'
|
|
15
|
+
└── hello.md
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 2. Frontmatter as Metadata
|
|
19
|
+
|
|
20
|
+
Use YAML frontmatter to add type-safe metadata to your content.
|
|
21
|
+
|
|
22
|
+
```markdown
|
|
23
|
+
---
|
|
24
|
+
title: My Title
|
|
25
|
+
tags: [news, update]
|
|
26
|
+
featured: true
|
|
27
|
+
---
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 3. Querying the Knowledge Core
|
|
31
|
+
|
|
32
|
+
Use the fluent API to retrieve and filter content.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
const content = c.get('content');
|
|
36
|
+
|
|
37
|
+
// Fetch a single post
|
|
38
|
+
const post = await content.collection('blog')
|
|
39
|
+
.slug('hello-world')
|
|
40
|
+
.fetch();
|
|
41
|
+
|
|
42
|
+
// List all items in a collection
|
|
43
|
+
const posts = await content.collection('blog').all();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 4. SSG Integration
|
|
47
|
+
|
|
48
|
+
Combine Monolith with `@gravito/prism` to build extremely fast static sites.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// During SSG phase
|
|
52
|
+
const posts = await content.collection('blog').all();
|
|
53
|
+
for (const post of posts) {
|
|
54
|
+
ssg.addPage(`/blog/${post.slug}`, 'blog-template', { post });
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 5. Performance: Memory Caching
|
|
59
|
+
|
|
60
|
+
Monolith caches the parsed markdown and metadata in memory. In a **Galaxy Architecture**, you can enable `Plasma` backing to share this cache across instances.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/monolith",
|
|
3
|
-
"
|
|
3
|
+
"sideEffects": false,
|
|
4
|
+
"version": "3.2.3",
|
|
4
5
|
"description": "Enterprise monolith framework for Gravito Galaxy",
|
|
5
6
|
"main": "dist/index.cjs",
|
|
6
7
|
"module": "dist/index.js",
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
}
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
|
-
"build": "tsup src/index.ts --format esm,cjs
|
|
18
|
+
"build": "tsup src/index.ts --format esm,cjs",
|
|
19
|
+
"build:dts": "tsup src/index.ts --format esm,cjs --dts",
|
|
18
20
|
"test": "bun test --timeout=10000",
|
|
19
21
|
"test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
|
|
20
22
|
"test:ci": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
|
|
@@ -31,16 +33,18 @@
|
|
|
31
33
|
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
32
34
|
"license": "MIT",
|
|
33
35
|
"dependencies": {
|
|
34
|
-
"@gravito/mass": "
|
|
36
|
+
"@gravito/mass": "^3.0.3",
|
|
35
37
|
"@octokit/rest": "^22.0.1",
|
|
36
|
-
"gray-matter": "^4.0.3"
|
|
38
|
+
"gray-matter": "^4.0.3"
|
|
39
|
+
},
|
|
40
|
+
"optionalDependencies": {
|
|
37
41
|
"marked": "^11.1.1"
|
|
38
42
|
},
|
|
39
43
|
"peerDependencies": {
|
|
40
|
-
"@gravito/core": "
|
|
44
|
+
"@gravito/core": "^2.0.0"
|
|
41
45
|
},
|
|
42
46
|
"devDependencies": {
|
|
43
|
-
"@gravito/core": "
|
|
47
|
+
"@gravito/core": "^2.0.6",
|
|
44
48
|
"@types/marked": "^5.0.0",
|
|
45
49
|
"bun-types": "^1.3.5",
|
|
46
50
|
"tsup": "^8.5.1",
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gravito/monolith",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Enterprise monolith framework for Gravito Galaxy",
|
|
5
|
+
"main": "dist/index.cjs",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
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'"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"gravito",
|
|
27
|
+
"orbit",
|
|
28
|
+
"monolith",
|
|
29
|
+
"backend"
|
|
30
|
+
],
|
|
31
|
+
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@gravito/mass": "workspace:*",
|
|
35
|
+
"@octokit/rest": "^22.0.1",
|
|
36
|
+
"gray-matter": "^4.0.3",
|
|
37
|
+
"marked": "^11.1.1"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@gravito/core": "workspace:*"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@gravito/core": "workspace:*",
|
|
44
|
+
"@types/marked": "^5.0.0",
|
|
45
|
+
"bun-types": "^1.3.5",
|
|
46
|
+
"tsup": "^8.5.1",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/gravito-framework/gravito#readme",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/gravito-framework/gravito.git",
|
|
56
|
+
"directory": "packages/monolith"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/ContentManager.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { join, parse } from 'node:path'
|
|
2
|
+
import {
|
|
3
|
+
createHtmlRenderCallbacks,
|
|
4
|
+
getEscapeHtml,
|
|
5
|
+
getMarkdownAdapter,
|
|
6
|
+
type MarkdownRenderCallbacks,
|
|
7
|
+
type RuntimeMarkdownAdapter,
|
|
8
|
+
} from '@gravito/core'
|
|
2
9
|
import matter from 'gray-matter'
|
|
3
|
-
import { marked } from 'marked'
|
|
4
10
|
import type { ContentDriver } from './driver/ContentDriver'
|
|
5
11
|
|
|
6
12
|
/**
|
|
@@ -26,6 +32,11 @@ export interface CollectionConfig {
|
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Manages fetching, parsing, and caching of filesystem-based content.
|
|
35
|
+
*
|
|
36
|
+
* 使用 RuntimeMarkdownAdapter 進行 Markdown 渲染,在 Bun 環境下
|
|
37
|
+
* 自動使用原生 C++ 解析器(10-100x 更快),並保留 XSS 防護的
|
|
38
|
+
* 自訂渲染回調。
|
|
39
|
+
*
|
|
29
40
|
* @public
|
|
30
41
|
*/
|
|
31
42
|
export class ContentManager {
|
|
@@ -34,19 +45,24 @@ export class ContentManager {
|
|
|
34
45
|
private cache = new Map<string, ContentItem>()
|
|
35
46
|
// In-memory search index: term -> Set<cacheKey>
|
|
36
47
|
private searchIndex = new Map<string, Set<string>>()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
// Escape HTML helper
|
|
50
|
+
private escapeHtml = getEscapeHtml()
|
|
51
|
+
|
|
52
|
+
// RuntimeMarkdownAdapter(自動選擇 Bun 原生或 marked fallback)
|
|
53
|
+
private readonly mdAdapter: RuntimeMarkdownAdapter = getMarkdownAdapter()
|
|
54
|
+
// 完整的 HTML 渲染回調(含 XSS 防護覆寫)
|
|
55
|
+
private readonly renderCallbacks: MarkdownRenderCallbacks = createHtmlRenderCallbacks({
|
|
56
|
+
html: (rawHtml: string) => this.escapeHtml(rawHtml),
|
|
57
|
+
link: (content: string, opts: { href: string; title?: string }) => {
|
|
58
|
+
if (!opts.href || !this.isSafeUrl(opts.href)) {
|
|
59
|
+
return content
|
|
43
60
|
}
|
|
44
|
-
const safeHref = this.escapeHtml(href)
|
|
45
|
-
const titleAttr = title ? ` title="${this.escapeHtml(title)}"` : ''
|
|
46
|
-
return `<a href="${safeHref}"${titleAttr}>${
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
})()
|
|
61
|
+
const safeHref = this.escapeHtml(opts.href)
|
|
62
|
+
const titleAttr = opts.title ? ` title="${this.escapeHtml(opts.title)}"` : ''
|
|
63
|
+
return `<a href="${safeHref}"${titleAttr}>${content}</a>`
|
|
64
|
+
},
|
|
65
|
+
})
|
|
50
66
|
|
|
51
67
|
/**
|
|
52
68
|
* Clear all cached content.
|
|
@@ -134,7 +150,8 @@ export class ContentManager {
|
|
|
134
150
|
const fileContent = await this.driver.read(filePath)
|
|
135
151
|
const { data, content, excerpt } = matter(fileContent)
|
|
136
152
|
|
|
137
|
-
|
|
153
|
+
// 使用 RuntimeMarkdownAdapter.render() 搭配完整 HTML 回調(含 XSS 防護)
|
|
154
|
+
const html = this.mdAdapter.render(content, this.renderCallbacks)
|
|
138
155
|
|
|
139
156
|
const item: ContentItem = {
|
|
140
157
|
slug,
|
|
@@ -272,15 +289,6 @@ export class ContentManager {
|
|
|
272
289
|
return value
|
|
273
290
|
}
|
|
274
291
|
|
|
275
|
-
private escapeHtml(value: string): string {
|
|
276
|
-
return value
|
|
277
|
-
.replace(/&/g, '&')
|
|
278
|
-
.replace(/</g, '<')
|
|
279
|
-
.replace(/>/g, '>')
|
|
280
|
-
.replace(/"/g, '"')
|
|
281
|
-
.replace(/'/g, ''')
|
|
282
|
-
}
|
|
283
|
-
|
|
284
292
|
private isSafeUrl(href: string): boolean {
|
|
285
293
|
const trimmed = href.trim()
|
|
286
294
|
if (!trimmed) {
|
package/src/ContentWatcher.ts
CHANGED
|
@@ -54,13 +54,12 @@ export class ContentWatcher {
|
|
|
54
54
|
clearTimeout(this.debounceTimers.get(key))
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
this.handleFileChange(collection, filename.toString(), localePrefix)
|
|
59
|
+
this.debounceTimers.delete(key)
|
|
60
|
+
}, this.options.debounceMs ?? 100)
|
|
61
|
+
timer.unref?.()
|
|
62
|
+
this.debounceTimers.set(key, timer)
|
|
64
63
|
})
|
|
65
64
|
|
|
66
65
|
this.watchers.push(watcher)
|
package/src/Router.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 路由註冊用的應用介面
|
|
3
|
+
*/
|
|
4
|
+
interface RoutableApp {
|
|
5
|
+
get(path: string, handler: unknown): unknown
|
|
6
|
+
post(path: string, handler: unknown): unknown
|
|
7
|
+
put(path: string, handler: unknown): unknown
|
|
8
|
+
delete(path: string, handler: unknown): unknown
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
12
|
* Utility for registering resourceful routes.
|
|
@@ -16,7 +24,7 @@ export class RouterHelper {
|
|
|
16
24
|
* PUT /prefix/:id -> update
|
|
17
25
|
* DELETE /prefix/:id -> destroy
|
|
18
26
|
*/
|
|
19
|
-
public static resource(app:
|
|
27
|
+
public static resource(app: RoutableApp, prefix: string, controller: any) {
|
|
20
28
|
const p = prefix.startsWith('/') ? prefix : `/${prefix}`
|
|
21
29
|
|
|
22
30
|
// Mapping: Method -> [HTTP Verb, Path Suffix]
|
|
@@ -32,7 +40,8 @@ export class RouterHelper {
|
|
|
32
40
|
|
|
33
41
|
for (const [method, [verb, suffix]] of Object.entries(routes)) {
|
|
34
42
|
if (typeof controller.prototype[method] === 'function') {
|
|
35
|
-
|
|
43
|
+
const route = app[verb as keyof RoutableApp] as (path: string, handler: unknown) => unknown
|
|
44
|
+
route.call(app, `${p}${suffix}`, controller.call(method))
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GravitoContext, GravitoNext } from '@gravito/core'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Automatically trim all strings in the request body and query.
|
|
5
5
|
*/
|
|
6
|
-
export const trimStrings = ()
|
|
7
|
-
return async (c, next) => {
|
|
6
|
+
export const trimStrings = () => {
|
|
7
|
+
return async (c: GravitoContext, next: GravitoNext) => {
|
|
8
8
|
// We proxy the req.json and req.query methods to return trimmed data
|
|
9
9
|
// This is more efficient than pre-processing everything if the controller doesn't use it.
|
|
10
10
|
|
|
@@ -13,7 +13,7 @@ export const trimStrings = (): MiddlewareHandler => {
|
|
|
13
13
|
try {
|
|
14
14
|
const body = await c.req.json()
|
|
15
15
|
clean(body, (val) => (typeof val === 'string' ? val.trim() : val))
|
|
16
|
-
// Since
|
|
16
|
+
// Since body is already read, we might need to store it in context
|
|
17
17
|
// But for now, let's assume we use a simpler approach for the prototype.
|
|
18
18
|
} catch {
|
|
19
19
|
// Skip if not valid JSON
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
2
|
import { existsSync } from 'node:fs'
|
|
3
3
|
import { mkdir, rm, writeFile } from 'node:fs/promises'
|
|
4
4
|
import { join } from 'node:path'
|
|
@@ -53,4 +53,17 @@ describe('ContentWatcher', () => {
|
|
|
53
53
|
|
|
54
54
|
watcher.close()
|
|
55
55
|
})
|
|
56
|
+
|
|
57
|
+
it('should call close without errors', async () => {
|
|
58
|
+
// Test basic functionality: ContentWatcher can be instantiated and closed
|
|
59
|
+
// without errors. The actual file watching behavior is tested in the first test.
|
|
60
|
+
const manager = new ContentManager(new LocalDriver(TMP_DIR))
|
|
61
|
+
manager.defineCollection('docs', { path: 'docs' })
|
|
62
|
+
|
|
63
|
+
const watcher = new ContentWatcher(manager, TMP_DIR, { debounceMs: 100 })
|
|
64
|
+
watcher.watch('docs')
|
|
65
|
+
|
|
66
|
+
// Verify watcher can be closed without errors
|
|
67
|
+
expect(() => watcher.close()).not.toThrow()
|
|
68
|
+
})
|
|
56
69
|
})
|
package/tests/content.test.ts
CHANGED
|
@@ -40,3 +40,63 @@ describe('Orbit Content Manager', () => {
|
|
|
40
40
|
expect(items[0].slug).toBe('install')
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
|
+
|
|
44
|
+
// ============ Phase 1 遷移驗證測試 ============
|
|
45
|
+
|
|
46
|
+
describe('ContentManager - RuntimeMarkdownAdapter 遷移', () => {
|
|
47
|
+
const rootDir = join(import.meta.dir, 'fixtures')
|
|
48
|
+
|
|
49
|
+
test('使用 RuntimeMarkdownAdapter 產生的 HTML 與既有行為一致', async () => {
|
|
50
|
+
const manager = new ContentManager(new LocalDriver(rootDir))
|
|
51
|
+
manager.defineCollection('docs', { path: 'docs' })
|
|
52
|
+
|
|
53
|
+
const item = await manager.find('docs', 'install', 'en')
|
|
54
|
+
expect(item).not.toBeNull()
|
|
55
|
+
// 關鍵驗證:HTML 輸出格式和 marked 一致
|
|
56
|
+
expect(item?.body).toContain('<h1>Installation</h1>')
|
|
57
|
+
expect(item?.body).toContain('<p>To install Gravito, run:</p>')
|
|
58
|
+
expect(item?.body).toContain('<pre><code class="language-bash">')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('自訂 link 渲染產生安全的 HTML', async () => {
|
|
62
|
+
const manager = new ContentManager(new LocalDriver(rootDir))
|
|
63
|
+
manager.defineCollection('docs', { path: 'docs' })
|
|
64
|
+
|
|
65
|
+
// ContentManager 的自訂 renderer 應過濾 javascript: URLs
|
|
66
|
+
// 透過 renderCallbacks 驗證自訂 link 渲染
|
|
67
|
+
const item = await manager.find('docs', 'install', 'en')
|
|
68
|
+
expect(item).not.toBeNull()
|
|
69
|
+
// body 中不應含有 javascript: 協議
|
|
70
|
+
expect(item?.body).not.toContain('javascript:')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('gray-matter frontmatter 解析在遷移後保持不變', async () => {
|
|
74
|
+
const manager = new ContentManager(new LocalDriver(rootDir))
|
|
75
|
+
manager.defineCollection('docs', { path: 'docs' })
|
|
76
|
+
|
|
77
|
+
const item = await manager.find('docs', 'install', 'en')
|
|
78
|
+
expect(item).not.toBeNull()
|
|
79
|
+
// Frontmatter 欄位完整
|
|
80
|
+
expect(item?.meta.title).toBe('Installation Guide')
|
|
81
|
+
expect(item?.meta.description).toBe('How to install Gravito')
|
|
82
|
+
expect(item?.meta.date).toBeDefined()
|
|
83
|
+
// raw 欄位應為 markdown 內容(不含 frontmatter)
|
|
84
|
+
expect(item?.raw).toContain('# Installation')
|
|
85
|
+
expect(item?.raw).not.toContain('---')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('快取機制在遷移後正常運作', async () => {
|
|
89
|
+
const manager = new ContentManager(new LocalDriver(rootDir))
|
|
90
|
+
manager.defineCollection('docs', { path: 'docs' })
|
|
91
|
+
|
|
92
|
+
// 第一次讀取
|
|
93
|
+
const item1 = await manager.find('docs', 'install', 'en')
|
|
94
|
+
// 第二次讀取(應從快取取得)
|
|
95
|
+
const item2 = await manager.find('docs', 'install', 'en')
|
|
96
|
+
|
|
97
|
+
expect(item1).not.toBeNull()
|
|
98
|
+
expect(item2).not.toBeNull()
|
|
99
|
+
// 快取應回傳相同物件
|
|
100
|
+
expect(item1).toBe(item2)
|
|
101
|
+
})
|
|
102
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
"extends": "../../tsconfig.json",
|
|
3
3
|
"compilerOptions": {
|
|
4
4
|
"outDir": "./dist",
|
|
5
|
-
"baseUrl": ".",
|
|
6
5
|
"skipLibCheck": true,
|
|
7
|
-
"types": ["bun-types"]
|
|
8
|
-
"paths": {
|
|
9
|
-
"@gravito/core": ["../../packages/core/src/index.ts"],
|
|
10
|
-
"@gravito/*": ["../../packages/*/src/index.ts"]
|
|
11
|
-
}
|
|
6
|
+
"types": ["bun-types"]
|
|
12
7
|
},
|
|
13
8
|
"include": ["src/**/*"],
|
|
14
9
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|