@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 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
- - **Markdown Support**: Full Markdown parsing via `marked`.
57
- - **Frontmatter**: Parses YAML frontmatter using `gray-matter`.
58
- - **Collections**: Organize content into collections (folders).
59
- - **Query API**: Fluent API to fetch by slug, list all, etc. `collection('posts').slug('...').fetch()`.
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
- renderer = (() => {
64
- const renderer = new import_marked.marked.Renderer();
65
- renderer.html = (html) => this.escapeHtml(html);
66
- renderer.link = (href, title, text) => {
67
- if (!href || !this.isSafeUrl(href)) {
68
- return text;
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}>${text}</a>`;
73
- };
74
- return renderer;
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 = await import_marked.marked.parse(content, { renderer: this.renderer });
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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
- this.debounceTimers.set(
319
- key,
320
- setTimeout(() => {
321
- this.handleFileChange(collection, filename.toString(), localePrefix);
322
- this.debounceTimers.delete(key);
323
- }, this.options.debounceMs ?? 100)
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[verb](`${p}${suffix}`, controller.call(method));
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 renderer;
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: Hono<any, any, any>, prefix: string, controller: any): void;
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 renderer;
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: Hono<any, any, any>, prefix: string, controller: any): void;
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
- renderer = (() => {
20
- const renderer = new marked.Renderer();
21
- renderer.html = (html) => this.escapeHtml(html);
22
- renderer.link = (href, title, text) => {
23
- if (!href || !this.isSafeUrl(href)) {
24
- return text;
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}>${text}</a>`;
29
- };
30
- return renderer;
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 = await marked.parse(content, { renderer: this.renderer });
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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
- 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
- );
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[verb](`${p}${suffix}`, controller.call(method));
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
- "version": "3.2.0",
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 --dts",
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": "workspace:*",
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": "workspace:*"
44
+ "@gravito/core": "^2.0.0"
41
45
  },
42
46
  "devDependencies": {
43
- "@gravito/core": "workspace:*",
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",
@@ -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
+ }
@@ -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
- private renderer = (() => {
38
- const renderer = new marked.Renderer()
39
- renderer.html = (html: string) => this.escapeHtml(html)
40
- renderer.link = (href: string | null, title: string | null, text: string) => {
41
- if (!href || !this.isSafeUrl(href)) {
42
- return text
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}>${text}</a>`
47
- }
48
- return renderer
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
- const html = await marked.parse(content, { renderer: this.renderer })
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, '&amp;')
278
- .replace(/</g, '&lt;')
279
- .replace(/>/g, '&gt;')
280
- .replace(/"/g, '&quot;')
281
- .replace(/'/g, '&#39;')
282
- }
283
-
284
292
  private isSafeUrl(href: string): boolean {
285
293
  const trimmed = href.trim()
286
294
  if (!trimmed) {
@@ -54,13 +54,12 @@ export class ContentWatcher {
54
54
  clearTimeout(this.debounceTimers.get(key))
55
55
  }
56
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
- )
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
- import type { Hono } from 'hono'
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: Hono<any, any, any>, prefix: string, controller: any) {
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
- ;(app as any)[verb](`${p}${suffix}`, controller.call(method))
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 { MiddlewareHandler } from 'hono'
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 = (): MiddlewareHandler => {
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 Hono's body is already read, we might need to store it in context
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
  })
@@ -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"]