cardbuddy 0.6.0
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 +211 -0
- package/dist/cli.js +76 -0
- package/dist/config.js +63 -0
- package/dist/core.js +2 -0
- package/dist/preview-editor/assets/apple-touch-icon-BQcES9QK.png +0 -0
- package/dist/preview-editor/assets/default-image-wzZYQ86K.png +0 -0
- package/dist/preview-editor/assets/favicon-16-Cg6Nrosk.png +0 -0
- package/dist/preview-editor/assets/favicon-32-B_1744sp.png +0 -0
- package/dist/preview-editor/assets/index-BYB0hngG.js +72 -0
- package/dist/preview-editor/assets/index-CpBZEQDy.css +1 -0
- package/dist/preview-editor/assets/site-icon-CFMif8z4.png +0 -0
- package/dist/preview-editor/assets/svg-symbols-B2exBrAr.svg +118 -0
- package/dist/preview-editor/index.html +17 -0
- package/dist/server-core/_vendor/finmall-env.mjs +96 -0
- package/dist/server-core/assetProxy.js +154 -0
- package/dist/server-core/bbox.js +153 -0
- package/dist/server-core/browser.js +108 -0
- package/dist/server-core/buildDataFromSchema.js +50 -0
- package/dist/server-core/cardFiles.js +188 -0
- package/dist/server-core/cardHistory.js +156 -0
- package/dist/server-core/cardImage.js +85 -0
- package/dist/server-core/cardImportMeta.js +139 -0
- package/dist/server-core/cardListCache.js +68 -0
- package/dist/server-core/cardOpenFolder.js +135 -0
- package/dist/server-core/cardSave.js +127 -0
- package/dist/server-core/cardSidecar.js +72 -0
- package/dist/server-core/cardSync.js +259 -0
- package/dist/server-core/cardValidate.js +134 -0
- package/dist/server-core/index.js +82 -0
- package/dist/server-core/logger.js +241 -0
- package/dist/server-core/nodeModels.js +52 -0
- package/dist/server-core/renderStatus.js +207 -0
- package/dist/server-core/screenshot.js +123 -0
- package/dist/server-core/sliceStatic.js +44 -0
- package/dist/standalone.js +447 -0
- package/dist/vendored/App.vue +2330 -0
- package/dist/vendored/api.js +142 -0
- package/dist/vendored/assets/apple-touch-icon.png +0 -0
- package/dist/vendored/assets/default-image.png +0 -0
- package/dist/vendored/assets/favicon-16.png +0 -0
- package/dist/vendored/assets/favicon-32.png +0 -0
- package/dist/vendored/assets/logo.png +0 -0
- package/dist/vendored/assets/site-icon.png +0 -0
- package/dist/vendored/assets/svg-symbols.svg +118 -0
- package/dist/vendored/assets/svgIcons/icons.svg +54 -0
- package/dist/vendored/assets/svgIcons/index.js +39 -0
- package/dist/vendored/components/AbsoluteLayout.vue +42 -0
- package/dist/vendored/components/AlertCardInstance.vue +58 -0
- package/dist/vendored/components/AlertCardLayer.vue +59 -0
- package/dist/vendored/components/ApiPayloadTree.vue +168 -0
- package/dist/vendored/components/AppToast.vue +37 -0
- package/dist/vendored/components/Button.vue +37 -0
- package/dist/vendored/components/Canvas.vue +65 -0
- package/dist/vendored/components/CardGallery.vue +243 -0
- package/dist/vendored/components/CardPreview.vue +72 -0
- package/dist/vendored/components/CardSelect.vue +200 -0
- package/dist/vendored/components/CheckGroup.vue +131 -0
- package/dist/vendored/components/Column.vue +25 -0
- package/dist/vendored/components/ComponentPalette.vue +294 -0
- package/dist/vendored/components/ConfirmDialog.vue +39 -0
- package/dist/vendored/components/CustomPlaceholder.vue +33 -0
- package/dist/vendored/components/DatePickerOverlay.vue +345 -0
- package/dist/vendored/components/DebugPanel.vue +806 -0
- package/dist/vendored/components/DeviceOverlay.vue +70 -0
- package/dist/vendored/components/Divider.vue +22 -0
- package/dist/vendored/components/Dropdown.vue +191 -0
- package/dist/vendored/components/EventEditor.vue +247 -0
- package/dist/vendored/components/GridLayout.vue +67 -0
- package/dist/vendored/components/HoverGuides.vue +122 -0
- package/dist/vendored/components/Icon.vue +34 -0
- package/dist/vendored/components/Image.vue +34 -0
- package/dist/vendored/components/Input.vue +90 -0
- package/dist/vendored/components/JsonTree.vue +516 -0
- package/dist/vendored/components/ListLayout.vue +68 -0
- package/dist/vendored/components/LogValue.vue +51 -0
- package/dist/vendored/components/Map.vue +110 -0
- package/dist/vendored/components/MultiColumn.vue +49 -0
- package/dist/vendored/components/MultiText.vue +58 -0
- package/dist/vendored/components/NodeInspector.vue +295 -0
- package/dist/vendored/components/PickerOverlay.vue +131 -0
- package/dist/vendored/components/ProgressSlider.vue +167 -0
- package/dist/vendored/components/PropControl.vue +241 -0
- package/dist/vendored/components/PropTree.vue +128 -0
- package/dist/vendored/components/RadioButton.vue +74 -0
- package/dist/vendored/components/RemotePreview.vue +171 -0
- package/dist/vendored/components/RenderTree.vue +437 -0
- package/dist/vendored/components/Root.vue +30 -0
- package/dist/vendored/components/Row.vue +25 -0
- package/dist/vendored/components/SelectionOverlay.vue +105 -0
- package/dist/vendored/components/StructureTree.vue +949 -0
- package/dist/vendored/components/SvgCode.vue +19 -0
- package/dist/vendored/components/SvgSymbols.vue +30 -0
- package/dist/vendored/components/Switch.vue +38 -0
- package/dist/vendored/components/Text.vue +32 -0
- package/dist/vendored/components/Video.vue +70 -0
- package/dist/vendored/components/ViewContainer.vue +52 -0
- package/dist/vendored/components/base.js +12 -0
- package/dist/vendored/components/icons.js +47 -0
- package/dist/vendored/components/index.js +58 -0
- package/dist/vendored/components/setters/SetBgPosition.vue +64 -0
- package/dist/vendored/components/setters/SetBgSize.vue +90 -0
- package/dist/vendored/components/setters/SetCode.vue +90 -0
- package/dist/vendored/components/setters/SetColor.vue +244 -0
- package/dist/vendored/components/setters/SetCustomSelect.vue +58 -0
- package/dist/vendored/components/setters/SetExpr.vue +107 -0
- package/dist/vendored/components/setters/SetGradient.vue +125 -0
- package/dist/vendored/components/setters/SetGrid9.vue +68 -0
- package/dist/vendored/components/setters/SetIconSelect.vue +67 -0
- package/dist/vendored/components/setters/SetIfForm.vue +61 -0
- package/dist/vendored/components/setters/SetImage.vue +43 -0
- package/dist/vendored/components/setters/SetList.vue +139 -0
- package/dist/vendored/components/setters/SetNumber.vue +118 -0
- package/dist/vendored/components/setters/SetNumberStyle.vue +113 -0
- package/dist/vendored/components/setters/SetPadding.vue +77 -0
- package/dist/vendored/components/setters/SetSegment.vue +160 -0
- package/dist/vendored/components/setters/SetSelect.vue +51 -0
- package/dist/vendored/components/setters/SetSwitch.vue +49 -0
- package/dist/vendored/components/setters/SetText.vue +52 -0
- package/dist/vendored/components/setters/props.js +23 -0
- package/dist/vendored/fidelity/baseline-messageOverview.json +87 -0
- package/dist/vendored/fidelity/calibration.json +29 -0
- package/dist/vendored/fidelity/fidelity-messageOverview-after.html +114 -0
- package/dist/vendored/fidelity/fidelity-messageOverview-after.json +272 -0
- package/dist/vendored/fidelity/fidelity-messageOverview.html +185 -0
- package/dist/vendored/fidelity/fidelity-messageOverview.json +336 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +262 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +502 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +201 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +417 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +125 -0
- package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +338 -0
- package/dist/vendored/index.html +16 -0
- package/dist/vendored/main.js +36 -0
- package/dist/vendored/meta/action-meta.json +141 -0
- package/dist/vendored/meta/component-meta.json +2714 -0
- package/dist/vendored/meta/directive-meta.json +40 -0
- package/dist/vendored/meta/style-meta.json +1086 -0
- package/dist/vendored/runtime/StyleParser.js +179 -0
- package/dist/vendored/runtime/adapter.js +166 -0
- package/dist/vendored/runtime/alert-card.js +125 -0
- package/dist/vendored/runtime/app-toast.js +33 -0
- package/dist/vendored/runtime/assets.js +16 -0
- package/dist/vendored/runtime/canvas-context.js +141 -0
- package/dist/vendored/runtime/card-runtime.js +289 -0
- package/dist/vendored/runtime/clipboard-node.js +83 -0
- package/dist/vendored/runtime/clipboard.js +29 -0
- package/dist/vendored/runtime/evaluate.js +152 -0
- package/dist/vendored/runtime/event-model.js +300 -0
- package/dist/vendored/runtime/hoverGuide.js +31 -0
- package/dist/vendored/runtime/jsapi-mock.js +256 -0
- package/dist/vendored/runtime/node-model.js +217 -0
- package/dist/vendored/runtime/property.js +364 -0
- package/dist/vendored/runtime/selectionOverlay.js +53 -0
- package/dist/vendored/runtime/toPreview.js +648 -0
- package/dist/vendored/standalone-main.js +36 -0
- package/dist/vendored/standalone.html +12 -0
- package/dist/vendored/styles/app.css +2565 -0
- package/dist/vendored/styles/card-gallery.css +234 -0
- package/dist/vendored/styles/card-select.css +281 -0
- package/dist/vendored/styles/components.css +383 -0
- package/dist/vendored/styles/dropdown.css +337 -0
- package/dist/vendored/styles/json-tree.css +308 -0
- package/dist/vendored/styles/node-inspector.css +1003 -0
- package/dist/vendored/styles/setters.css +962 -0
- package/dist/vendored/styles/standalone.css +27 -0
- package/dist/vendored/styles/structure-tree.css +320 -0
- package/package.json +42 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cardbuddy 本地开发服务(Phase 1 骨架)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:把 dsl-card 后端的卡片增删查改能力从 Vite dev-server 抽离为独立进程——
|
|
5
|
+
* 监听 127.0.0.1,挂载 server/core 的全部中间件(sync / card / save / validate /
|
|
6
|
+
* render-status / node-models / import-meta / screenshot),并提供:
|
|
7
|
+
* - 反射 Origin 的 CORS(公网域名/IP 不确定也无妨)
|
|
8
|
+
* - 可选 x-dev-token 鉴权(默认仅靠 127.0.0.1 绑定)
|
|
9
|
+
* - /__events(SSE)实时推送 cards-changed(替代 Vite HMR)
|
|
10
|
+
* - 内嵌 Vite middlewareMode 实例,仅为 render-status / node-models 提供 ssrLoadModule
|
|
11
|
+
*
|
|
12
|
+
* 仅绑定 127.0.0.1;FinMall 凭据只在本机,公网前端永不接触。
|
|
13
|
+
*/
|
|
14
|
+
import http from 'node:http'
|
|
15
|
+
import https from 'node:https'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
import connect from 'connect'
|
|
20
|
+
import { createServer as createViteServer } from 'vite'
|
|
21
|
+
import vue from '@vitejs/plugin-vue'
|
|
22
|
+
import chokidar from 'chokidar'
|
|
23
|
+
import chalk from 'chalk'
|
|
24
|
+
|
|
25
|
+
import { mountCoreMiddlewares } from './core.js'
|
|
26
|
+
import { loadConfig } from './config.js'
|
|
27
|
+
|
|
28
|
+
const MIME_TYPES = {
|
|
29
|
+
'.html': 'text/html; charset=utf-8',
|
|
30
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
31
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
32
|
+
'.css': 'text/css; charset=utf-8',
|
|
33
|
+
'.json': 'application/json; charset=utf-8',
|
|
34
|
+
'.png': 'image/png',
|
|
35
|
+
'.jpg': 'image/jpeg',
|
|
36
|
+
'.jpeg': 'image/jpeg',
|
|
37
|
+
'.webp': 'image/webp',
|
|
38
|
+
'.gif': 'image/gif',
|
|
39
|
+
'.svg': 'image/svg+xml',
|
|
40
|
+
'.ico': 'image/x-icon',
|
|
41
|
+
'.woff': 'font/woff',
|
|
42
|
+
'.woff2': 'font/woff2',
|
|
43
|
+
'.ttf': 'font/ttf',
|
|
44
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
45
|
+
'.map': 'application/json; charset=utf-8',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decodePathname(url) {
|
|
49
|
+
if (!url) return '/'
|
|
50
|
+
const raw = url.split('?')[0]
|
|
51
|
+
try {
|
|
52
|
+
return decodeURIComponent(raw)
|
|
53
|
+
} catch {
|
|
54
|
+
return raw
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolvePreviewRoot(config) {
|
|
59
|
+
if (config.previewRoot) {
|
|
60
|
+
const explicit = path.resolve(config.previewRoot)
|
|
61
|
+
if (fs.existsSync(explicit)) return explicit
|
|
62
|
+
throw new Error(`DSL_PREVIEW_ROOT 指定的前端构建产物不存在:${explicit}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 发布态优先使用 npm 包内自带的 preview-editor(旧包布局 offline-editor 作兼容回退),
|
|
66
|
+
// 其次回退到仓库根 dist/(源码运行、未打包内置前端时)。
|
|
67
|
+
if (fs.existsSync(config.previewEditorDir)) return config.previewEditorDir
|
|
68
|
+
if (fs.existsSync(config.legacyOfflineEditorDir)) return config.legacyOfflineEditorDir
|
|
69
|
+
if (fs.existsSync(config.repoDistDir)) return config.repoDistDir
|
|
70
|
+
|
|
71
|
+
throw new Error(
|
|
72
|
+
'--preview 需要前端构建产物。请先在仓库根运行 `npm run build`,' +
|
|
73
|
+
'或重新执行 cardbuddy 的 `npm run build` 打包内置前端。'
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function injectPreviewConfig(html, apiBase) {
|
|
78
|
+
// 同时注入新旧两个全局名:已构建好的旧 dist/ 前端只认 __CARD_DEV_OFFLINE_BASE__,
|
|
79
|
+
// 新前端优先读 __CARD_DEV_PREVIEW_BASE__(见 preview/api.js),升级期间两者都能起。
|
|
80
|
+
const script =
|
|
81
|
+
`<script>window.__CARD_DEV_PREVIEW_BASE__=${JSON.stringify(apiBase)};` +
|
|
82
|
+
`window.__CARD_DEV_OFFLINE_BASE__=${JSON.stringify(apiBase)};</script>`
|
|
83
|
+
if (html.includes('<head>')) return html.replace('<head>', `<head>${script}`)
|
|
84
|
+
return `${script}${html}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sendFile(res, filePath, { inject = null } = {}) {
|
|
88
|
+
res.statusCode = 200
|
|
89
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
90
|
+
res.setHeader('Content-Type', MIME_TYPES[ext] || 'application/octet-stream')
|
|
91
|
+
if (path.basename(filePath) === 'index.html') {
|
|
92
|
+
res.setHeader('Cache-Control', 'no-store')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (inject !== null) {
|
|
96
|
+
const html = injectPreviewConfig(fs.readFileSync(filePath, 'utf8'), inject)
|
|
97
|
+
res.setHeader('Content-Length', Buffer.byteLength(html))
|
|
98
|
+
res.end(html)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fs.createReadStream(filePath).pipe(res)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createPreviewStaticMiddleware(root, apiBase) {
|
|
106
|
+
const indexFile = path.join(root, 'index.html')
|
|
107
|
+
if (!fs.existsSync(indexFile)) {
|
|
108
|
+
throw new Error(`preview 前端缺少 index.html:${indexFile}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (req, res, next) => {
|
|
112
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
|
|
113
|
+
|
|
114
|
+
let pathname = decodePathname(req.url)
|
|
115
|
+
// 兼容线上 /cardBuddy/index.html 习惯;本地实际按根路径托管 build 产物。
|
|
116
|
+
if (pathname === '/cardBuddy' || pathname === '/cardBuddy/') pathname = '/'
|
|
117
|
+
else if (pathname.startsWith('/cardBuddy/')) pathname = pathname.slice('/cardBuddy'.length) || '/'
|
|
118
|
+
|
|
119
|
+
// API/SSE 等内部端点交给后端中间件,不能走 SPA fallback。
|
|
120
|
+
if (pathname.startsWith('/__')) return next()
|
|
121
|
+
|
|
122
|
+
const relativePath = pathname.replace(/^\/+/, '')
|
|
123
|
+
let filePath = path.join(root, relativePath)
|
|
124
|
+
if (!filePath.startsWith(root + path.sep) && filePath !== root) {
|
|
125
|
+
res.statusCode = 403
|
|
126
|
+
return res.end('Forbidden')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let stat = null
|
|
130
|
+
try {
|
|
131
|
+
stat = fs.statSync(filePath)
|
|
132
|
+
} catch {
|
|
133
|
+
stat = null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (stat?.isDirectory()) {
|
|
137
|
+
filePath = path.join(filePath, 'index.html')
|
|
138
|
+
try {
|
|
139
|
+
stat = fs.statSync(filePath)
|
|
140
|
+
} catch {
|
|
141
|
+
stat = null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (stat?.isFile()) {
|
|
146
|
+
if (req.method === 'HEAD') {
|
|
147
|
+
res.statusCode = 200
|
|
148
|
+
res.setHeader('Content-Type', MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream')
|
|
149
|
+
return res.end()
|
|
150
|
+
}
|
|
151
|
+
// 所有 HTML 都注入 preview apiBase,避免只在首页注入、路由刷新后丢失。
|
|
152
|
+
const inject = path.extname(filePath).toLowerCase() === '.html' ? apiBase : null
|
|
153
|
+
return sendFile(res, filePath, { inject })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// SPA fallback:无扩展名路径返回 index.html;真正的资源 404 交给后续处理。
|
|
157
|
+
if (!path.extname(pathname)) {
|
|
158
|
+
if (req.method === 'HEAD') return res.end()
|
|
159
|
+
return sendFile(res, indexFile, { inject: apiBase })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
next()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function openBrowser(url) {
|
|
167
|
+
let command
|
|
168
|
+
let args
|
|
169
|
+
if (process.platform === 'darwin') {
|
|
170
|
+
command = 'open'
|
|
171
|
+
args = [url]
|
|
172
|
+
} else if (process.platform === 'win32') {
|
|
173
|
+
command = 'cmd'
|
|
174
|
+
args = ['/c', 'start', '', url]
|
|
175
|
+
} else {
|
|
176
|
+
command = 'xdg-open'
|
|
177
|
+
args = [url]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
182
|
+
child.on('error', () => {})
|
|
183
|
+
child.unref()
|
|
184
|
+
} catch {
|
|
185
|
+
// 自动打开浏览器失败不影响服务启动。
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 解析嵌入式 Vite 的「运行时根目录」与别名。
|
|
191
|
+
*
|
|
192
|
+
* 优先级(决定 SSR 端点 ssrLoadModule('/components/...') 落到哪里):
|
|
193
|
+
* 1) DSL_EDITOR_SRC 显式设置 → 用该编辑器源码目录(内核开发 / 调试用);
|
|
194
|
+
* 2) 包内 vendoredRuntimeDir 存在 → 用发布快照(默认、消费者场景,无需持有编辑器源码);
|
|
195
|
+
* 3) cwd 下存在 preview/ → 仓库内开发回退(无需先 build 编辑器即可起本地服务);
|
|
196
|
+
* 4) 兜底 → 仍用 vendored(不存在则由调用方捕获报错,给出明确提示)。
|
|
197
|
+
*
|
|
198
|
+
* 无论哪种,都把 `@` 别名指向运行时根,确保内部 `@/runtime/*`、`@/components/*` 解析一致。
|
|
199
|
+
* @returns {{ root: string, aliasAt: string }}
|
|
200
|
+
*/
|
|
201
|
+
function resolveRuntimeRoot(config) {
|
|
202
|
+
if (config.editorSrc) {
|
|
203
|
+
return { root: config.editorSrc, aliasAt: config.editorSrc }
|
|
204
|
+
}
|
|
205
|
+
if (fs.existsSync(config.vendoredRuntimeDir)) {
|
|
206
|
+
return { root: config.vendoredRuntimeDir, aliasAt: config.vendoredRuntimeDir }
|
|
207
|
+
}
|
|
208
|
+
if (fs.existsSync(config.repoPreviewDir)) {
|
|
209
|
+
return { root: config.repoPreviewDir, aliasAt: config.repoPreviewDir }
|
|
210
|
+
}
|
|
211
|
+
// 兜底:vendored(即便不存在,createEmbeddedVite 会抛清晰错误)
|
|
212
|
+
return { root: config.vendoredRuntimeDir, aliasAt: config.vendoredRuntimeDir }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 内嵌 Vite(middlewareMode)只为 SSR 端点提供 ssrLoadModule。 */
|
|
216
|
+
async function createEmbeddedVite(config) {
|
|
217
|
+
const base = {
|
|
218
|
+
server: { middlewareMode: true, hmr: false, ws: false },
|
|
219
|
+
appType: 'custom',
|
|
220
|
+
logLevel: 'warn',
|
|
221
|
+
}
|
|
222
|
+
const { root, aliasAt } = resolveRuntimeRoot(config)
|
|
223
|
+
// 若运行时根下存在 vite.config(如 DSL_EDITOR_SRC 指向完整仓库 preview),优先复用其配置,
|
|
224
|
+
// 保证 vue 插件 + 全部别名与编辑器完全一致;否则用最小配置(vue 插件 + @ 别名)。
|
|
225
|
+
const repoConfig = path.join(root, 'vite.config.mjs')
|
|
226
|
+
if (fs.existsSync(repoConfig)) {
|
|
227
|
+
return createViteServer({ configFile: repoConfig, ...base })
|
|
228
|
+
}
|
|
229
|
+
return createViteServer({
|
|
230
|
+
root,
|
|
231
|
+
configFile: false,
|
|
232
|
+
plugins: [vue()],
|
|
233
|
+
resolve: {
|
|
234
|
+
alias: {
|
|
235
|
+
'@': aliasAt,
|
|
236
|
+
// 与仓库 vite.config 对齐:@cards 指向消费者项目根的 src/cards
|
|
237
|
+
'@cards': path.join(config.root, 'src', 'cards'),
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
...base,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 反射 Origin 的 CORS + OPTIONS 预检处理。 */
|
|
245
|
+
function corsMiddleware(req, res, next) {
|
|
246
|
+
const origin = req.headers.origin
|
|
247
|
+
if (origin) res.setHeader('Access-Control-Allow-Origin', origin)
|
|
248
|
+
res.setHeader('Access-Control-Allow-Credentials', 'false')
|
|
249
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS')
|
|
250
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,x-dev-token')
|
|
251
|
+
res.setHeader('Access-Control-Expose-Headers', 'Content-Type')
|
|
252
|
+
// Private Network Access(PNA)预检应答:即便未来 Chrome 对私有/回环地址收紧,
|
|
253
|
+
// 带上该头可避免 OPTIONS 预检被拒。(回环 127.0.0.1 当前免 PNA,但带上零成本、向前兼容。)
|
|
254
|
+
res.setHeader('Access-Control-Allow-Private-Network', 'true')
|
|
255
|
+
if (req.method === 'OPTIONS') {
|
|
256
|
+
res.statusCode = 204
|
|
257
|
+
return res.end()
|
|
258
|
+
}
|
|
259
|
+
next()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 可选 token 鉴权。 */
|
|
263
|
+
function tokenMiddleware(token) {
|
|
264
|
+
return (req, res, next) => {
|
|
265
|
+
if (req.method === 'OPTIONS') return next()
|
|
266
|
+
if (req.headers['x-dev-token'] !== token) {
|
|
267
|
+
res.statusCode = 403
|
|
268
|
+
res.setHeader('Content-Type', 'application/json')
|
|
269
|
+
return res.end(JSON.stringify({ error: 'invalid x-dev-token' }))
|
|
270
|
+
}
|
|
271
|
+
next()
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** SSE 端点:订阅者集合 + 推送 helper。 */
|
|
276
|
+
function createEventsEndpoint() {
|
|
277
|
+
const clients = new Set()
|
|
278
|
+
const handler = (req, res, next) => {
|
|
279
|
+
if (!req.url || req.url.split('?')[0] !== '/__events') return next()
|
|
280
|
+
res.statusCode = 200
|
|
281
|
+
res.setHeader('Content-Type', 'text/event-stream')
|
|
282
|
+
res.setHeader('Cache-Control', 'no-cache')
|
|
283
|
+
res.setHeader('Connection', 'keep-alive')
|
|
284
|
+
res.flushHeaders?.()
|
|
285
|
+
res.write('retry: 3000\n\n')
|
|
286
|
+
const ping = setInterval(() => res.write(': ping\n\n'), 15000)
|
|
287
|
+
clients.add(res)
|
|
288
|
+
req.on('close', () => {
|
|
289
|
+
clearInterval(ping)
|
|
290
|
+
clients.delete(res)
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
const notify = (data) => {
|
|
294
|
+
const payload = `event: cards-changed\ndata: ${JSON.stringify(data)}\n\n`
|
|
295
|
+
for (const res of clients) res.write(payload)
|
|
296
|
+
}
|
|
297
|
+
return { handler, notify }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 监听 src/cards 变更并(防抖)推送。
|
|
302
|
+
*
|
|
303
|
+
* 用 chokidar 替代裸 fs.watch:Windows 下 fs.watch 的 recursive 递归监听不稳定
|
|
304
|
+
* (深层子目录改动易漏报、编辑器「临时文件改名覆盖」常漏捕、事件可能丢失文件名),
|
|
305
|
+
* 而这正是「连接本地开发服务后,外部改文件偶尔不自动刷新」的根因。chokidar 跨平台
|
|
306
|
+
* 稳定,与 Vite 内置监听同源。保持既有的 120ms 防抖、.history 过滤与
|
|
307
|
+
* file=relative(root,file) 发送格式不变(前端零改动)。
|
|
308
|
+
*/
|
|
309
|
+
function watchCards(root, notify) {
|
|
310
|
+
const cardsDir = path.join(root, 'src', 'cards')
|
|
311
|
+
if (!fs.existsSync(cardsDir)) return
|
|
312
|
+
let timer = null
|
|
313
|
+
const onChange = (file) => {
|
|
314
|
+
if (!file || !file.startsWith(cardsDir) || !/\.json$/.test(file)) return
|
|
315
|
+
if (file.split(path.sep).includes('.history')) return // 忽略编辑历史快照
|
|
316
|
+
// 忽略 data.json:它是 schema 的单向派生产物,被写盘时总会随 schema 一起重算,
|
|
317
|
+
// 若把它当作变更源会重复触发预览重载(且外部直接改 data.json 不应影响系统)。
|
|
318
|
+
if (path.basename(file) === 'data.json') return
|
|
319
|
+
if (timer) clearTimeout(timer)
|
|
320
|
+
timer = setTimeout(() => {
|
|
321
|
+
timer = null
|
|
322
|
+
notify({ file: path.relative(root, file) })
|
|
323
|
+
}, 120)
|
|
324
|
+
}
|
|
325
|
+
const watcher = chokidar.watch(cardsDir, {
|
|
326
|
+
ignoreInitial: true,
|
|
327
|
+
// 忽略编辑历史快照目录(src/cards/<id>/.history/):快照落盘会写 .json,
|
|
328
|
+
// 若触发 cards-changed → 预览自动 loadCard 重载卡片,会丢弃未保存编辑并关闭属性面板。
|
|
329
|
+
ignored: /(^|[/\\])\.history([/\\]|$)/,
|
|
330
|
+
depth: Infinity,
|
|
331
|
+
})
|
|
332
|
+
watcher.on('add', (f) => onChange(f))
|
|
333
|
+
watcher.on('change', (f) => onChange(f))
|
|
334
|
+
watcher.on('unlink', (f) => onChange(f))
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 解析 HTTPS 凭据(DSL_DEV_HTTPS=1 时启用)。
|
|
339
|
+
*
|
|
340
|
+
* - 首次生成自签证书(SAN 覆盖 localhost / 127.0.0.1 / ::1),缓存到
|
|
341
|
+
* `<root>/.local/dsl-dev-tls/`,后续重启复用(避免每次换指纹、浏览器需反复信任)。
|
|
342
|
+
* - 自签证书不在系统信任链,浏览器首次访问会告警;用户需在浏览器打开
|
|
343
|
+
* `https://127.0.0.1:<port>/__cards` 并接受一次警告,之后编辑器跨源 fetch 即放行。
|
|
344
|
+
* - 仅 lazy-import selfsigned,HTTP 模式(默认)完全不触碰该依赖。
|
|
345
|
+
*
|
|
346
|
+
* @param {ReturnType<typeof loadConfig>} config
|
|
347
|
+
* @returns {Promise<{ key: string, cert: string } | null>}
|
|
348
|
+
*/
|
|
349
|
+
async function resolveTls(config) {
|
|
350
|
+
if (!config.https) return null
|
|
351
|
+
const dir = path.join(config.root, '.local', 'dsl-dev-tls')
|
|
352
|
+
const keyPath = path.join(dir, 'key.pem')
|
|
353
|
+
const certPath = path.join(dir, 'cert.pem')
|
|
354
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
355
|
+
return {
|
|
356
|
+
key: fs.readFileSync(keyPath, 'utf8'),
|
|
357
|
+
cert: fs.readFileSync(certPath, 'utf8'),
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const selfsigned = (await import('selfsigned')).default
|
|
361
|
+
const attrs = [
|
|
362
|
+
{ name: 'commonName', value: 'localhost' },
|
|
363
|
+
{ name: 'subjectAltName', value: 'DNS:localhost,IP:127.0.0.1,IP:::1' },
|
|
364
|
+
]
|
|
365
|
+
const pems = selfsigned.generate(attrs, { days: 365, keySize: 2048, algorithm: 'sha256' })
|
|
366
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
367
|
+
fs.writeFileSync(keyPath, pems.private)
|
|
368
|
+
fs.writeFileSync(certPath, pems.cert)
|
|
369
|
+
return { key: pems.private, cert: pems.cert }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 启动本地开发服务。
|
|
374
|
+
* @param {Partial<ReturnType<typeof loadConfig>>} [userConfig]
|
|
375
|
+
* @returns {Promise<{ server: import('node:http').Server, vite: any, config: object }>}
|
|
376
|
+
*/
|
|
377
|
+
export async function startServer(userConfig = {}) {
|
|
378
|
+
const config = { ...loadConfig(), ...userConfig }
|
|
379
|
+
const app = connect()
|
|
380
|
+
|
|
381
|
+
app.use(corsMiddleware)
|
|
382
|
+
|
|
383
|
+
const tls = await resolveTls(config)
|
|
384
|
+
const scheme = tls ? 'https' : 'http'
|
|
385
|
+
const apiBase = `${scheme}://127.0.0.1:${config.port}`
|
|
386
|
+
|
|
387
|
+
// preview 页面需要绕过 token(浏览器直接导航无法带 x-dev-token);API 仍在 token 之后保护。
|
|
388
|
+
if (config.preview) {
|
|
389
|
+
const previewRoot = resolvePreviewRoot(config)
|
|
390
|
+
app.use(createPreviewStaticMiddleware(previewRoot, apiBase))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (config.token) app.use(tokenMiddleware(config.token))
|
|
394
|
+
|
|
395
|
+
const events = createEventsEndpoint()
|
|
396
|
+
app.use(events.handler)
|
|
397
|
+
|
|
398
|
+
const vite = await createEmbeddedVite(config)
|
|
399
|
+
// upstream(FINMALL_BASE_URL)透传给资源代理中间件,使其能回源 FinMall 取站内资源
|
|
400
|
+
mountCoreMiddlewares(app, { root: config.root, viteServer: vite, upstream: config.upstream })
|
|
401
|
+
|
|
402
|
+
watchCards(config.root, events.notify)
|
|
403
|
+
|
|
404
|
+
const server = tls
|
|
405
|
+
? https.createServer(tls, app)
|
|
406
|
+
: http.createServer(app)
|
|
407
|
+
await new Promise((resolve, reject) => {
|
|
408
|
+
server.once('error', reject)
|
|
409
|
+
server.listen(config.port, '127.0.0.1', () => resolve())
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
const bind = `${scheme}://127.0.0.1:${config.port}`
|
|
413
|
+
// 被 dev.mjs 拉起时(DSL_LAUNCHER='dev'),来源标签由启动器用带背景色的 chip 标注,
|
|
414
|
+
// 这里让出前缀避免双重来源标签;直接运行 / dev:prod(stdio:inherit)则自带标签。
|
|
415
|
+
const srcTag =
|
|
416
|
+
process.env.DSL_LAUNCHER === 'dev'
|
|
417
|
+
? ''
|
|
418
|
+
: chalk.magenta('▌ ') + chalk.magenta.bold('cardbuddy') + ' '
|
|
419
|
+
console.log(
|
|
420
|
+
srcTag +
|
|
421
|
+
chalk.green(`本地开发服务已启动:${bind}`) +
|
|
422
|
+
chalk.dim(
|
|
423
|
+
` (root=${config.root},token=${config.token ? 'on' : 'off(仅127.0.0.1)'})`
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
if (tls) {
|
|
427
|
+
console.log(
|
|
428
|
+
srcTag +
|
|
429
|
+
chalk.dim(
|
|
430
|
+
`自签证书已缓存于 ${path.join(config.root, '.local', 'dsl-dev-tls')}` +
|
|
431
|
+
` ;首次使用请先在浏览器打开 ${bind}/__cards 并接受一次安全警告,之后编辑器跨源调用即放行。`
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (config.preview) {
|
|
437
|
+
const previewUrl = `${bind}/`
|
|
438
|
+
console.log(srcTag + chalk.cyan(`本地预览编辑器已启动:${previewUrl}`) + chalk.dim(' (正在打开浏览器)'))
|
|
439
|
+
openBrowser(previewUrl)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const shutdown = async () => {
|
|
443
|
+
server.close()
|
|
444
|
+
await vite.close().catch(() => {})
|
|
445
|
+
}
|
|
446
|
+
return { server, vite, config, shutdown }
|
|
447
|
+
}
|