@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 +6 -4
- package/package.json +15 -2
- package/src/index.mjs +125 -125
package/dsh-suite.json
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
"title": "Editor Host",
|
|
6
6
|
"description": "通用编辑器宿主:静态托管、租约、心跳、连接状态。",
|
|
7
7
|
"packages": {
|
|
8
|
-
"
|
|
8
|
+
"@capyclaw/editor-host": {
|
|
9
9
|
"role": "provider",
|
|
10
|
-
"version": "^0.1.
|
|
10
|
+
"version": "^0.1.1",
|
|
11
11
|
"path": ".",
|
|
12
12
|
"provides": {
|
|
13
|
-
"services": [
|
|
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": "
|
|
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.
|
|
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
|
+
}
|