@capyclaw/editor-host 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,37 @@
1
- # Temporary Holding Version
1
+ # dsh-editor-host
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ DSH 公共编辑器宿主插件。功能插件只提供 `editor/` 静态目录,不再自己写静态路由、租约和连接状态。
4
+
5
+ ## 使用
6
+
7
+ ```js
8
+ export const inject = ['editorHost', 'serviceHost']
9
+
10
+ export function apply(ctx, config) {
11
+ const editor = ctx.editorHost.createEditorHost({
12
+ id: 'my-editor',
13
+ dir: path.join(packageDir, 'editor'),
14
+ basePath: '/editor',
15
+ files: { '/': 'index.html', '/app.js': 'app.js' },
16
+ })
17
+ const app = ctx.serviceHost.createApp({
18
+ id: 'my-plugin',
19
+ health: () => editor.status(),
20
+ })
21
+ for (const route of editor.routes()) app.use(route)
22
+ }
23
+ ```
24
+
25
+ ## 提供什么
26
+
27
+ - `createEditorHost({ id, dir, basePath, index, files })`
28
+ - `routes()`:静态文件 + 租约端点
29
+ - `status()`:`{ connected, lease }`,可直接并入 `/healthz`
30
+ - `createLease()`:`dsh-plugin-kit/editor/lease` 的租约仲裁
31
+ - 端点:`POST <base>/claim`、`POST <base>/release`、`GET <base>/status`
32
+
33
+ ## 约定
34
+
35
+ - “显示即连接”:页面加载后自动 claim;后打开者强制抢占
36
+ - 租约状态只用于内部仲裁,不渲染成“未连接/待命”
37
+ - 心跳间隔应小于 TTL(默认 3s / 15s)
@@ -0,0 +1,4 @@
1
+ # 由 dsh-editor-host 提供;功能插件通过 inject: ['editorHost'] 或 ctx.get('editorHost') 使用。
2
+ - insert:
3
+ - id: editor-host
4
+ name: "dsh-editor-host"
package/dsh-suite.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "../dsh-suite-kit/suite.schema.json",
3
+ "name": "dsh-editor-host-suite",
4
+ "version": "0.1.0",
5
+ "title": "Editor Host",
6
+ "description": "通用编辑器宿主:静态托管、租约、心跳、连接状态。",
7
+ "packages": {
8
+ "@capyclaw/editor-host": {
9
+ "role": "provider",
10
+ "version": "^0.1.1",
11
+ "path": ".",
12
+ "provides": {
13
+ "services": [
14
+ "editorHost"
15
+ ],
16
+ "capabilities": {
17
+ "editor.host": "1.0.0"
18
+ }
19
+ }
20
+ }
21
+ },
22
+ "rows": [
23
+ {
24
+ "id": "editor-host",
25
+ "name": "@capyclaw/editor-host"
26
+ }
27
+ ],
28
+ "checks": []
29
+ }
package/lib/host.mjs ADDED
@@ -0,0 +1,11 @@
1
+ /** dsh-editor-host 插件:向同进程插件提供 ctx.editorHost。 */
2
+ import { createEditorHostService } from '../src/index.mjs'
3
+
4
+ export const name = 'editor-host'
5
+
6
+ export function apply(ctx, config = {}) {
7
+ const service = createEditorHostService(config)
8
+ const dispose = ctx.provide('editorHost', service)
9
+ console.log('[editor-host] ready')
10
+ return () => { if (typeof dispose === 'function') dispose() }
11
+ }
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@capyclaw/editor-host",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "DSH 公共编辑器宿主插件:静态托管、租约仲裁、心跳与连接状态,向插件提供 ctx.editorHost。",
5
+ "type": "module",
6
+ "main": "./lib/host.mjs",
7
+ "exports": {
8
+ ".": "./lib/host.mjs",
9
+ "./src": "./src/index.mjs",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "engines": {
13
+ "node": ">=18",
14
+ "dsh": "^0.2.0-rc.2"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dsh": {
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ },
23
+ "suite": "./dsh-suite.json",
24
+ "apiVersion": "1.0.0",
25
+ "category": "service",
26
+ "platform": "both"
27
+ },
28
+ "meta": {
29
+ "title": "公共编辑器宿主",
30
+ "description": "静态托管、租约仲裁、心跳与连接状态,向插件提供 ctx.editorHost"
31
+ },
32
+ "files": [
33
+ "lib/",
34
+ "src/",
35
+ "cordis.patch.yml",
36
+ "dsh-suite.json",
37
+ "README.md"
38
+ ],
39
+ "license": "MIT",
40
+ "scripts": {
41
+ "test": "node --test test/unit.test.mjs"
42
+ },
43
+ "dependencies": {
44
+ "@capyclaw/plugin-kit": "^0.2.0"
45
+ }
46
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * dsh-editor-host —— 通用浏览器编辑器宿主。
3
+ *
4
+ * 功能插件只提供 editor/ 静态目录,不再自己写:
5
+ * - 静态文件路由 / content-type / 404
6
+ * - “显示即连接”的租约仲裁(后打开者抢占,旧窗口静默待命)
7
+ * - claim / release / status / heartbeat 端点
8
+ * - 连接状态供 /healthz 使用
9
+ *
10
+ * 浏览器端可直接使用 @capyclaw/plugin-kit 的 editor/lease.mjs(无 Node 依赖)。
11
+ */
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+ import { createEditorLease } from '@capyclaw/plugin-kit/editor/lease'
15
+ import { send, readBody } from '@capyclaw/plugin-kit/service/http'
16
+ import { createServiceApi } from '@capyclaw/plugin-kit/service/api'
17
+
18
+ const MIME = {
19
+ '.html': 'text/html; charset=utf-8',
20
+ '.js': 'text/javascript; charset=utf-8',
21
+ '.mjs': 'text/javascript; charset=utf-8',
22
+ '.css': 'text/css; charset=utf-8',
23
+ '.json': 'application/json; charset=utf-8',
24
+ '.svg': 'image/svg+xml',
25
+ '.png': 'image/png',
26
+ '.jpg': 'image/jpeg',
27
+ '.jpeg': 'image/jpeg',
28
+ '.webp': 'image/webp',
29
+ '.woff2': 'font/woff2',
30
+ }
31
+
32
+ function safeJoin(root, rel) {
33
+ const target = path.resolve(root, `.${path.sep}${rel}`)
34
+ const base = path.resolve(root)
35
+ return target === base || target.startsWith(base + path.sep) ? target : null
36
+ }
37
+
38
+ function contentType(file) {
39
+ return MIME[path.extname(file).toLowerCase()] || 'application/octet-stream'
40
+ }
41
+
42
+ /**
43
+ * @param {{ id: string, dir: string, basePath?: string, index?: string, files?: Record<string,string|string[]> }} options
44
+ */
45
+ export function createEditorHost({ id, dir, basePath = '', index = 'index.html', files = null } = {}) {
46
+ if (!id) throw new Error('createEditorHost: id 必填')
47
+ const prefix = basePath === '/' ? '' : String(basePath || '').replace(/\/+$/, '')
48
+ const lease = createEditorLease()
49
+
50
+ const staticRoutes = async function editorStatic(req, res, u) {
51
+ if (req.method !== 'GET' && req.method !== 'HEAD') return false
52
+ if (prefix && u.pathname !== prefix && !u.pathname.startsWith(prefix + '/')) return false
53
+ const relRaw = prefix ? u.pathname.slice(prefix.length) : u.pathname
54
+ const rel = relRaw === '' || relRaw === '/' ? index : decodeURIComponent(relRaw.replace(/^\/+/, ''))
55
+ let file = null
56
+ if (files) {
57
+ const hit = files[relRaw === '' ? '/' : relRaw] || files[relRaw]
58
+ if (!hit) return false
59
+ const name = Array.isArray(hit) ? hit[0] : hit
60
+ file = safeJoin(dir, name)
61
+ } else {
62
+ file = safeJoin(dir, rel)
63
+ }
64
+ if (!file) return false
65
+ try {
66
+ const data = fs.readFileSync(file)
67
+ res.writeHead(200, {
68
+ 'content-type': contentType(file),
69
+ 'content-length': data.length,
70
+ 'x-content-type-options': 'nosniff',
71
+ 'referrer-policy': 'no-referrer',
72
+ 'cache-control': 'no-cache',
73
+ })
74
+ res.end(req.method === 'HEAD' ? undefined : data)
75
+ return true
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ const leaseRoutes = async function editorLeaseRoutes(req, res, u) {
82
+ const claimPath = `${prefix}/claim`
83
+ const releasePath = `${prefix}/release`
84
+ const statusPath = `${prefix}/status`
85
+ if (u.pathname === claimPath && req.method === 'POST') {
86
+ const body = await readBody(req)
87
+ const result = lease.claim(String(body.id || ''), !!body.force)
88
+ send(req, res, result.ok ? 200 : 409, result)
89
+ return true
90
+ }
91
+ if (u.pathname === releasePath && req.method === 'POST') {
92
+ const body = await readBody(req)
93
+ send(req, res, 200, lease.release(String(body.id || '')))
94
+ return true
95
+ }
96
+ if (u.pathname === statusPath && req.method === 'GET') {
97
+ send(req, res, 200, { ok: true, connected: lease.isLive(), lease: lease.status() })
98
+ return true
99
+ }
100
+ return false
101
+ }
102
+
103
+ return {
104
+ id,
105
+ lease,
106
+ routes() {
107
+ return [staticRoutes, leaseRoutes]
108
+ },
109
+ staticRoutes,
110
+ leaseRoutes,
111
+ status() {
112
+ return { connected: lease.isLive(), lease: lease.status() }
113
+ },
114
+ }
115
+ }
116
+
117
+ export function createEditorHostService(config = {}) {
118
+ return {
119
+ api: createServiceApi('editorHost', '1.0.0', ['static', 'lease', 'heartbeat', 'status']),
120
+ version: '0.1.0',
121
+ createEditorHost,
122
+ createLease: createEditorLease,
123
+ defaultHost: config.dir ? createEditorHost({ id: config.id || 'editor', ...config }) : null,
124
+ }
125
+ }