@capyclaw/editor-host 0.1.0 → 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/dsh-suite.json CHANGED
@@ -5,12 +5,14 @@
5
5
  "title": "Editor Host",
6
6
  "description": "通用编辑器宿主:静态托管、租约、心跳、连接状态。",
7
7
  "packages": {
8
- "dsh-editor-host": {
8
+ "@capyclaw/editor-host": {
9
9
  "role": "provider",
10
- "version": "^0.1.0",
10
+ "version": "^0.1.1",
11
11
  "path": ".",
12
12
  "provides": {
13
- "services": ["editorHost"],
13
+ "services": [
14
+ "editorHost"
15
+ ],
14
16
  "capabilities": {
15
17
  "editor.host": "1.0.0"
16
18
  }
@@ -20,7 +22,7 @@
20
22
  "rows": [
21
23
  {
22
24
  "id": "editor-host",
23
- "name": "dsh-editor-host"
25
+ "name": "@capyclaw/editor-host"
24
26
  }
25
27
  ],
26
28
  "checks": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capyclaw/editor-host",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DSH 公共编辑器宿主插件:静态托管、租约仲裁、心跳与连接状态,向插件提供 ctx.editorHost。",
5
5
  "type": "module",
6
6
  "main": "./lib/host.mjs",
@@ -9,12 +9,25 @@
9
9
  "./src": "./src/index.mjs",
10
10
  "./package.json": "./package.json"
11
11
  },
12
+ "engines": {
13
+ "node": ">=18",
14
+ "dsh": "^0.2.0-rc.2"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
12
19
  "dsh": {
13
20
  "bundle": {
14
21
  "patch": "./cordis.patch.yml"
15
22
  },
16
23
  "suite": "./dsh-suite.json",
17
- "apiVersion": "1.0.0"
24
+ "apiVersion": "1.0.0",
25
+ "category": "service",
26
+ "platform": "both"
27
+ },
28
+ "meta": {
29
+ "title": "公共编辑器宿主",
30
+ "description": "静态托管、租约仲裁、心跳与连接状态,向插件提供 ctx.editorHost"
18
31
  },
19
32
  "files": [
20
33
  "lib/",
package/src/index.mjs CHANGED
@@ -1,125 +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
- }
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
+ }