@changfenhuang/dsh-genui 0.9.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/CHANGELOG.md +367 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh-CN.md +261 -0
- package/SKILL.md +115 -0
- package/cordis.patch.yml +9 -0
- package/demo-prompts.md +118 -0
- package/lib/assets/echarts.js +64 -0
- package/lib/assets/mermaid.js +3591 -0
- package/lib/assets/three.js +3844 -0
- package/lib/client.js +5 -0
- package/lib/index.js +1895 -0
- package/lib/invariant.js +19 -0
- package/lib/types/client/EChartNode.d.ts +4 -0
- package/lib/types/client/ErrorBoundary.d.ts +30 -0
- package/lib/types/client/GenuiBlock.d.ts +7 -0
- package/lib/types/client/PlotBlock.d.ts +44 -0
- package/lib/types/client/action-context.d.ts +7 -0
- package/lib/types/client/asset-echarts.d.ts +15 -0
- package/lib/types/client/asset-loader.d.ts +31 -0
- package/lib/types/client/asset-mermaid.d.ts +1 -0
- package/lib/types/client/asset-three.d.ts +1 -0
- package/lib/types/client/blocks/advanced.d.ts +104 -0
- package/lib/types/client/blocks/basic.d.ts +27 -0
- package/lib/types/client/blocks/charts.d.ts +38 -0
- package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
- package/lib/types/client/blocks/diagram/index.d.ts +5 -0
- package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
- package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
- package/lib/types/client/blocks/forms.d.ts +83 -0
- package/lib/types/client/blocks/render-node.d.ts +10 -0
- package/lib/types/client/blocks/state.d.ts +58 -0
- package/lib/types/client/dom-fence.d.ts +14 -0
- package/lib/types/client/echarts-lazy.d.ts +17 -0
- package/lib/types/client/fence-render.d.ts +58 -0
- package/lib/types/client/gallery.d.ts +9 -0
- package/lib/types/client/guard.d.ts +126 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/interaction-store.d.ts +43 -0
- package/lib/types/client/mermaid-core.d.ts +8 -0
- package/lib/types/client/mermaid-lazy.d.ts +9 -0
- package/lib/types/client/mermaid-safe.d.ts +52 -0
- package/lib/types/client/panel-command.d.ts +29 -0
- package/lib/types/client/panel-store.d.ts +94 -0
- package/lib/types/client/panel.d.ts +15 -0
- package/lib/types/client/parse-partial.d.ts +37 -0
- package/lib/types/client/safe-math.d.ts +43 -0
- package/lib/types/client/scene3d-core.d.ts +21 -0
- package/lib/types/client/scene3d-lazy.d.ts +8 -0
- package/lib/types/client/spec.d.ts +610 -0
- package/lib/types/client/toolview.d.ts +7 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/plugin/index.d.ts +25 -0
- package/lib/types/plugin/invariant.d.ts +15 -0
- package/lib/types/plugin/tool.d.ts +27 -0
- package/lib/types/shared/fence-repair.d.ts +67 -0
- package/package.json +121 -0
- package/src/client/EChartNode.tsx +242 -0
- package/src/client/ErrorBoundary.tsx +66 -0
- package/src/client/GenuiBlock.module.css +1263 -0
- package/src/client/GenuiBlock.tsx +166 -0
- package/src/client/PlotBlock.module.css +155 -0
- package/src/client/PlotBlock.tsx +431 -0
- package/src/client/action-context.ts +32 -0
- package/src/client/asset-echarts.ts +25 -0
- package/src/client/asset-loader.ts +79 -0
- package/src/client/asset-mermaid.ts +13 -0
- package/src/client/asset-three.ts +12 -0
- package/src/client/blocks/advanced.tsx +435 -0
- package/src/client/blocks/basic.tsx +116 -0
- package/src/client/blocks/charts.tsx +319 -0
- package/src/client/blocks/diagram/geometry.ts +151 -0
- package/src/client/blocks/diagram/index.tsx +284 -0
- package/src/client/blocks/diagram/layout.ts +199 -0
- package/src/client/blocks/diagram/theme.ts +108 -0
- package/src/client/blocks/forms.tsx +523 -0
- package/src/client/blocks/render-node.tsx +260 -0
- package/src/client/blocks/state.ts +61 -0
- package/src/client/dom-fence.tsx +641 -0
- package/src/client/echarts-lazy.ts +35 -0
- package/src/client/fence-render.tsx +230 -0
- package/src/client/gallery.ts +128 -0
- package/src/client/guard.ts +1450 -0
- package/src/client/index.tsx +195 -0
- package/src/client/interaction-store.ts +122 -0
- package/src/client/mermaid-core.ts +121 -0
- package/src/client/mermaid-lazy.ts +31 -0
- package/src/client/mermaid-safe.ts +122 -0
- package/src/client/panel-command.ts +119 -0
- package/src/client/panel-store.ts +471 -0
- package/src/client/panel.tsx +185 -0
- package/src/client/parse-partial.ts +162 -0
- package/src/client/safe-math.ts +270 -0
- package/src/client/scene3d-core.ts +192 -0
- package/src/client/scene3d-lazy.ts +26 -0
- package/src/client/spec.ts +767 -0
- package/src/client/toolview.tsx +66 -0
- package/src/css-modules.d.ts +5 -0
- package/src/index.ts +12 -0
- package/src/plugin/index.ts +173 -0
- package/src/plugin/invariant.ts +30 -0
- package/src/plugin/node-shims.d.ts +31 -0
- package/src/plugin/tool.ts +330 -0
- package/src/shared/fence-repair.ts +244 -0
- package/tsconfig.json +35 -0
- package/tsdown.config.ts +193 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fence-body JSON repair — pure string functions, no DOM, no I/O.
|
|
3
|
+
* Used by BOTH the client fence renderer (tier-1/tier-2 auto-repair before
|
|
4
|
+
* rendering) and the node-side validate_dsh_ui tool (which returns the
|
|
5
|
+
* repaired JSON to the model instead of making it re-author the fix).
|
|
6
|
+
*
|
|
7
|
+
* Two tiers, deliberately gated differently by the callers:
|
|
8
|
+
* - Tier-1 (`repairFenceJson`): heals the most common model JSON typos that
|
|
9
|
+
* do NOT change the body's structure — unescaped half-width quotes inside
|
|
10
|
+
* string values and trailing commas. Safe at any time (streaming included),
|
|
11
|
+
* adopted only when the WHOLE body parses afterwards.
|
|
12
|
+
* - Tier-2 (`completeFenceJson`): heals structural incompleteness — missing
|
|
13
|
+
* closing quotes/brackets — by appending the missing terminators, and
|
|
14
|
+
* skips mismatched closers (a `]` mistyped as `}`, duplicated terminators).
|
|
15
|
+
* SETTLED MESSAGES ONLY: a streaming half must never be adopted as a
|
|
16
|
+
* finished prefix.
|
|
17
|
+
* @module @changfenhuang/dsh-genui/shared/fence-repair
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** A fence body counts as complete when it parses as a whole JSON value. */
|
|
21
|
+
export function isCompleteJson(raw: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
JSON.parse(raw)
|
|
24
|
+
return true
|
|
25
|
+
} catch {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Short human-readable reason for a body that fails whole-JSON parsing, or
|
|
31
|
+
* null when it parses. Positions come from the host's JSON.parse error. */
|
|
32
|
+
export function describeJsonFailure(raw: string): string | null {
|
|
33
|
+
try {
|
|
34
|
+
JSON.parse(raw)
|
|
35
|
+
return null
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
38
|
+
const pos = msg.match(/position (\d+)/i)
|
|
39
|
+
const where = pos !== null ? `(字符 ${pos[1]} 附近)` : ''
|
|
40
|
+
return `${where}${msg.slice(0, 140)}`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tier-1 repair — SAFE AT ANY TIME (streaming included): heals the most
|
|
46
|
+
* common model JSON typos that do NOT change the body's structure, and only
|
|
47
|
+
* when the whole body parses afterwards (so a still-growing streaming half
|
|
48
|
+
* can never be adopted):
|
|
49
|
+
*
|
|
50
|
+
* 1. Unescaped half-width `"` inside a string value — Chinese text quoted
|
|
51
|
+
* with ASCII quotes (e.g. `对"别名路径"判定失败`), which makes JSON.parse
|
|
52
|
+
* fail near that quote with "Expected ',' or ']'...".
|
|
53
|
+
* 2. Trailing commas before `}` / `]` or at end of input.
|
|
54
|
+
*
|
|
55
|
+
* The state-machine scan walks the raw body tracking string-open state:
|
|
56
|
+
* - inside a string, a quote whose next non-space char is NOT one of `, ] } :`
|
|
57
|
+
* (or end of input) cannot legally close the string → escape it as `\"`;
|
|
58
|
+
* - a `,` whose next non-space char is `}` / `]` / end of input is a trailing
|
|
59
|
+
* comma → drop it.
|
|
60
|
+
*
|
|
61
|
+
* Returns `{ text, repairs }` on success, or null when nothing needed fixing
|
|
62
|
+
* or the body still does not parse (callers fall through to tier-2 / banner).
|
|
63
|
+
*/
|
|
64
|
+
export function repairFenceJson(raw: string): { text: string; repairs: number } | null {
|
|
65
|
+
try {
|
|
66
|
+
JSON.parse(raw)
|
|
67
|
+
return null
|
|
68
|
+
} catch {
|
|
69
|
+
// fall through to the repair scan
|
|
70
|
+
}
|
|
71
|
+
let out = ''
|
|
72
|
+
let inString = false
|
|
73
|
+
let escaped = false
|
|
74
|
+
let repairs = 0
|
|
75
|
+
for (let i = 0; i < raw.length; i++) {
|
|
76
|
+
const ch = raw[i]
|
|
77
|
+
if (escaped) {
|
|
78
|
+
out += ch
|
|
79
|
+
escaped = false
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
if (inString && ch === '\\') {
|
|
83
|
+
out += ch
|
|
84
|
+
escaped = true
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
if (ch === '"') {
|
|
88
|
+
if (!inString) {
|
|
89
|
+
inString = true
|
|
90
|
+
out += ch
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
// Inside a string: is this quote the terminator? Look past whitespace.
|
|
94
|
+
let j = i + 1
|
|
95
|
+
while (j < raw.length && (raw[j] === ' ' || raw[j] === '\t' || raw[j] === '\n' || raw[j] === '\r')) j++
|
|
96
|
+
const next = j < raw.length ? raw[j] : ''
|
|
97
|
+
if (next === ',' || next === ']' || next === '}' || next === ':' || next === '') {
|
|
98
|
+
inString = false
|
|
99
|
+
out += ch
|
|
100
|
+
} else {
|
|
101
|
+
// Free-standing quote inside a value → escape it.
|
|
102
|
+
out += '\\"'
|
|
103
|
+
repairs++
|
|
104
|
+
}
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (ch === ',') {
|
|
108
|
+
// Trailing comma before `}` / `]` / end of input → drop it.
|
|
109
|
+
let j = i + 1
|
|
110
|
+
while (j < raw.length && (raw[j] === ' ' || raw[j] === '\t' || raw[j] === '\n' || raw[j] === '\r')) j++
|
|
111
|
+
const next = j < raw.length ? raw[j] : ''
|
|
112
|
+
if (next === '}' || next === ']' || next === '') {
|
|
113
|
+
repairs++
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
out += ch
|
|
118
|
+
}
|
|
119
|
+
if (repairs === 0) return null
|
|
120
|
+
try {
|
|
121
|
+
JSON.parse(out)
|
|
122
|
+
return { text: out, repairs }
|
|
123
|
+
} catch {
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Tier-2 repair — SETTLED MESSAGES ONLY (never while streaming): heals
|
|
130
|
+
* structural incompleteness — missing closing quotes/brackets — by appending
|
|
131
|
+
* the missing terminators, and heals stray closers — a `]` mistyped as `}` or
|
|
132
|
+
* a duplicated terminator — by skipping closers that do not match the open
|
|
133
|
+
* stack (they cannot be legal JSON). Callers gate it on settled messages (the
|
|
134
|
+
* client uses the host-provided fence source; the validate tool is by
|
|
135
|
+
* definition pre-emission), so a streaming half can never flash premature UI.
|
|
136
|
+
*
|
|
137
|
+
* ONE unified scan: the tier-1 fixes (quote escaping + trailing-comma drops)
|
|
138
|
+
* are folded into the same pass, so bodies that combine BOTH defect classes
|
|
139
|
+
* (a trailing comma AND a missing closer) heal in one shot — the old
|
|
140
|
+
* two-phase chain lost tier-1's partial work when its whole-body parse
|
|
141
|
+
* failed, and re-scanning the raw text could not compose the repairs.
|
|
142
|
+
* Adopted only when the completed body parses as whole JSON.
|
|
143
|
+
*/
|
|
144
|
+
export function completeFenceJson(raw: string): { text: string; repairs: number } | null {
|
|
145
|
+
try {
|
|
146
|
+
JSON.parse(raw)
|
|
147
|
+
return null
|
|
148
|
+
} catch {
|
|
149
|
+
// fall through to the unified repair scan
|
|
150
|
+
}
|
|
151
|
+
let out = ''
|
|
152
|
+
const stack: Array<'}' | ']'> = []
|
|
153
|
+
let inString = false
|
|
154
|
+
let escaped = false
|
|
155
|
+
let repairs = 0
|
|
156
|
+
for (let i = 0; i < raw.length; i++) {
|
|
157
|
+
const ch = raw[i]
|
|
158
|
+
if (escaped) {
|
|
159
|
+
out += ch
|
|
160
|
+
escaped = false
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
if (inString) {
|
|
164
|
+
if (ch === '\\') {
|
|
165
|
+
out += ch
|
|
166
|
+
escaped = true
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (ch !== '"') {
|
|
170
|
+
out += ch
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
// Inside a string: is this quote the terminator? Look past whitespace.
|
|
174
|
+
let j = i + 1
|
|
175
|
+
while (j < raw.length && (raw[j] === ' ' || raw[j] === '\t' || raw[j] === '\n' || raw[j] === '\r')) j++
|
|
176
|
+
const next = j < raw.length ? raw[j] : ''
|
|
177
|
+
if (next === ',' || next === ']' || next === '}' || next === ':' || next === '') {
|
|
178
|
+
inString = false
|
|
179
|
+
out += ch
|
|
180
|
+
} else {
|
|
181
|
+
// Free-standing quote inside a value → escape it (tier-1 fix).
|
|
182
|
+
out += '\\"'
|
|
183
|
+
repairs++
|
|
184
|
+
}
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
if (ch === '"') {
|
|
188
|
+
inString = true
|
|
189
|
+
out += ch
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
if (ch === '{') {
|
|
193
|
+
stack.push('}')
|
|
194
|
+
out += ch
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
if (ch === '[') {
|
|
198
|
+
stack.push(']')
|
|
199
|
+
out += ch
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (ch === '}' || ch === ']') {
|
|
203
|
+
if (stack[stack.length - 1] === ch) {
|
|
204
|
+
stack.pop()
|
|
205
|
+
out += ch
|
|
206
|
+
} else {
|
|
207
|
+
// Mismatched closer (e.g. a `]` mistyped as `}`, or a duplicated
|
|
208
|
+
// terminator): no legal JSON can contain it here, so skip it and let
|
|
209
|
+
// the remaining closers pair up again. The whole-body parse below is
|
|
210
|
+
// the final arbiter — if skipping made things worse, nothing is
|
|
211
|
+
// adopted and the diagnostic banner stays.
|
|
212
|
+
repairs++
|
|
213
|
+
}
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
if (ch === ',') {
|
|
217
|
+
// Trailing comma before `}` / `]` / end of input → drop it (tier-1 fix).
|
|
218
|
+
let j = i + 1
|
|
219
|
+
while (j < raw.length && (raw[j] === ' ' || raw[j] === '\t' || raw[j] === '\n' || raw[j] === '\r')) j++
|
|
220
|
+
const next = j < raw.length ? raw[j] : ''
|
|
221
|
+
if (next === '}' || next === ']' || next === '') {
|
|
222
|
+
repairs++
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
out += ch
|
|
227
|
+
}
|
|
228
|
+
if (inString) {
|
|
229
|
+
// Unterminated string value → close it.
|
|
230
|
+
out += '"'
|
|
231
|
+
repairs++
|
|
232
|
+
}
|
|
233
|
+
while (stack.length > 0) {
|
|
234
|
+
out += stack.pop()
|
|
235
|
+
repairs++
|
|
236
|
+
}
|
|
237
|
+
if (repairs === 0) return null
|
|
238
|
+
try {
|
|
239
|
+
JSON.parse(out)
|
|
240
|
+
return { text: out, repairs }
|
|
241
|
+
} catch {
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"lib": [
|
|
8
|
+
"ES2024",
|
|
9
|
+
"DOM",
|
|
10
|
+
"DOM.Iterable"
|
|
11
|
+
],
|
|
12
|
+
"types": [
|
|
13
|
+
"node"
|
|
14
|
+
],
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"allowImportingTsExtensions": true,
|
|
19
|
+
"rewriteRelativeImportExtensions": true,
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"exactOptionalPropertyTypes": true,
|
|
23
|
+
"noImplicitOverride": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUnusedLocals": true,
|
|
26
|
+
"noUnusedParameters": true,
|
|
27
|
+
"rootDir": "src",
|
|
28
|
+
"outDir": "lib",
|
|
29
|
+
"emitDeclarationOnly": true,
|
|
30
|
+
"declarationDir": "lib/types"
|
|
31
|
+
},
|
|
32
|
+
"include": [
|
|
33
|
+
"src"
|
|
34
|
+
]
|
|
35
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-genui build: node-half lib (host plugin, prompt injection) + browser
|
|
3
|
+
* client bundle (the dsh-ui renderer) speaking the dsh module-loader
|
|
4
|
+
* protocol (`window.__ModuleLoader__.load`). Mirrors the dsh repo's
|
|
5
|
+
* packages/client/tsdown.client.ts preset, simplified for one package.
|
|
6
|
+
*
|
|
7
|
+
* Deterministic output: the CSS Modules class map is emitted in fixed
|
|
8
|
+
* UTF-16 local-name order (never localeCompare — system-locale drift), and
|
|
9
|
+
* the production browser bundle carries no sourcemap, so the same source
|
|
10
|
+
* builds byte-identical client.js every time.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from 'node:fs/promises'
|
|
13
|
+
import { basename, dirname, relative, resolve as resolvePath } from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
import type { UserConfig } from 'tsdown'
|
|
16
|
+
import { transform } from 'lightningcss'
|
|
17
|
+
|
|
18
|
+
const ID = '@changfenhuang/dsh-genui'
|
|
19
|
+
const PROJECT_ROOT = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
|
|
21
|
+
/** Module-table entries this bundle may leave external: platform seed rows
|
|
22
|
+
* (react family, cordis, ui-primitives) answered by the loader's require.
|
|
23
|
+
* `react-dom/client` joins the list for the DOM render channel (pure-plugin
|
|
24
|
+
* fence rendering on pristine hosts mounts its own React roots). */
|
|
25
|
+
const EXTERNALS = [
|
|
26
|
+
'react', 'react/jsx-runtime', 'react-dom/client', '@deepseek-ai/cordis', '@deepseek-ai/dsh-client-ui-primitives',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
|
|
30
|
+
const CSS_VIRTUAL_SUFFIX = '.mjs'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function cssModulesPlugin(): NonNullable<UserConfig['plugins']>[number] {
|
|
34
|
+
return {
|
|
35
|
+
name: 'dsh-css-modules-inline',
|
|
36
|
+
resolveId(source: string, importer: string | undefined) {
|
|
37
|
+
if (!source.endsWith('.module.css')) return null
|
|
38
|
+
// The bundle builds straight from src, so the importer's directory is
|
|
39
|
+
// always the source tree — no lib/types backtracking needed.
|
|
40
|
+
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source
|
|
41
|
+
const stableId = relative(PROJECT_ROOT, abs).replaceAll('\\', '/')
|
|
42
|
+
return CSS_VIRTUAL_PREFIX + stableId + CSS_VIRTUAL_SUFFIX
|
|
43
|
+
},
|
|
44
|
+
async load(virtualId: string) {
|
|
45
|
+
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null
|
|
46
|
+
const stableId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
|
|
47
|
+
const fileId = resolvePath(PROJECT_ROOT, stableId)
|
|
48
|
+
this.addWatchFile(fileId)
|
|
49
|
+
const source = await readFile(fileId)
|
|
50
|
+
const { code, exports: cssExports } = transform({
|
|
51
|
+
filename: stableId,
|
|
52
|
+
code: source,
|
|
53
|
+
cssModules: { pattern: '[hash]_[local]' },
|
|
54
|
+
minify: true,
|
|
55
|
+
})
|
|
56
|
+
// Deterministic key order: fixed UTF-16 comparison on the LOCAL class
|
|
57
|
+
// names (localeCompare depends on the system locale). Values unchanged.
|
|
58
|
+
const entries = Object.entries(cssExports ?? {})
|
|
59
|
+
.map(([local, exp]) => [local, exp.name] as const)
|
|
60
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
61
|
+
const classMap = Object.fromEntries(entries)
|
|
62
|
+
const tagId = `${ID}/${basename(fileId)}`
|
|
63
|
+
return [
|
|
64
|
+
`const css = ${JSON.stringify(code.toString())};`,
|
|
65
|
+
`const tagId = ${JSON.stringify(tagId)};`,
|
|
66
|
+
`if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {`,
|
|
67
|
+
` const tag = document.createElement('style');`,
|
|
68
|
+
` tag.dataset.plugin = ${JSON.stringify(ID)};`,
|
|
69
|
+
' tag.dataset.pluginCss = tagId;',
|
|
70
|
+
' tag.textContent = css;',
|
|
71
|
+
' document.head.appendChild(tag);',
|
|
72
|
+
'}',
|
|
73
|
+
`export default ${JSON.stringify(classMap)};`,
|
|
74
|
+
].join('\n')
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function purityGate(): NonNullable<UserConfig['plugins']>[number] {
|
|
80
|
+
return {
|
|
81
|
+
name: 'dsh-client-bundle-purity',
|
|
82
|
+
resolveId(source: string) {
|
|
83
|
+
if (!source.startsWith('@deepseek-ai/')) return null
|
|
84
|
+
if (EXTERNALS.includes(source)) return null
|
|
85
|
+
throw new Error(
|
|
86
|
+
`client bundle purity: "${source}" is not in the module table (EXTERNALS) — `
|
|
87
|
+
+ 'cross-plugin value imports are forbidden; collaborate through cordis services',
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const clientConfig: UserConfig = {
|
|
94
|
+
name: `${ID}/client`,
|
|
95
|
+
entry: { client: 'src/client/index.tsx' },
|
|
96
|
+
outDir: 'lib',
|
|
97
|
+
format: 'cjs',
|
|
98
|
+
platform: 'browser',
|
|
99
|
+
dts: false,
|
|
100
|
+
// Minified production bundles: the host serves plugin bytes uncompressed,
|
|
101
|
+
// so every byte counts at first-load time.
|
|
102
|
+
minify: true,
|
|
103
|
+
// No sourcemap in the production bundle: nothing ships the map, nothing
|
|
104
|
+
// rewrites paths, builds are byte-identical.
|
|
105
|
+
sourcemap: false,
|
|
106
|
+
clean: false,
|
|
107
|
+
deps: {
|
|
108
|
+
neverBundle: [...EXTERNALS],
|
|
109
|
+
alwaysBundle: (id: string) => !EXTERNALS.includes(id),
|
|
110
|
+
},
|
|
111
|
+
define: {
|
|
112
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
113
|
+
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
114
|
+
'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
|
|
115
|
+
},
|
|
116
|
+
plugins: [purityGate(), cssModulesPlugin()],
|
|
117
|
+
outputOptions: {
|
|
118
|
+
entryFileNames: 'client.js',
|
|
119
|
+
// The loader fetches one script per plugin; the lazy mermaid/three
|
|
120
|
+
// engines are NOT part of this bundle anymore — they ship as standalone
|
|
121
|
+
// IIFE assets under lib/assets/, fetched on demand by the runtime loaders
|
|
122
|
+
// (see asset-loader.ts). Nothing heavy may re-enter this bundle.
|
|
123
|
+
codeSplitting: false,
|
|
124
|
+
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(ID)}, factory: (require) => {`,
|
|
125
|
+
footer: 'return module.exports; } });',
|
|
126
|
+
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Lazy engine assets: mermaid and three ship as standalone single-entry
|
|
132
|
+
* IIFEs that register themselves on `window.__GenuiAssets__` (no
|
|
133
|
+
* module-loader protocol — the loaders read the global directly after script
|
|
134
|
+
* injection). Two configs because rolldown refuses IIFE with multiple
|
|
135
|
+
* entries; each bundle is fully independent (no cross-entry chunks). The
|
|
136
|
+
* main client bundle never contains either engine, so the eager download
|
|
137
|
+
* drops from ~9 MB to the small renderer core.
|
|
138
|
+
*/
|
|
139
|
+
function assetConfig(name: 'mermaid' | 'three' | 'echarts', entry: string): UserConfig {
|
|
140
|
+
return {
|
|
141
|
+
name: `${ID}/assets/${name}`,
|
|
142
|
+
entry: { [`assets/${name}`]: entry },
|
|
143
|
+
outDir: 'lib',
|
|
144
|
+
format: 'iife',
|
|
145
|
+
platform: 'browser',
|
|
146
|
+
dts: false,
|
|
147
|
+
// Minified: these engines are huge unminified (6.7 MB / 1.8 MB) and the
|
|
148
|
+
// host serves uncompressed bytes; on-demand loads still want them small.
|
|
149
|
+
minify: true,
|
|
150
|
+
sourcemap: false,
|
|
151
|
+
clean: false,
|
|
152
|
+
deps: {
|
|
153
|
+
neverBundle: [...EXTERNALS],
|
|
154
|
+
alwaysBundle: (id: string) => !EXTERNALS.includes(id),
|
|
155
|
+
},
|
|
156
|
+
define: {
|
|
157
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
158
|
+
},
|
|
159
|
+
plugins: [purityGate()],
|
|
160
|
+
outputOptions: {
|
|
161
|
+
entryFileNames: '[name].js',
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const libConfig: UserConfig = {
|
|
167
|
+
name: ID,
|
|
168
|
+
// Named entries keep the published layout stable (lib/index.js +
|
|
169
|
+
// lib/invariant.js) while src/index.ts becomes the canonical bundle
|
|
170
|
+
// source entry that plugin_check's tool-bundle contract expects.
|
|
171
|
+
entry: { index: 'src/index.ts', invariant: 'src/plugin/invariant.ts' },
|
|
172
|
+
outDir: 'lib',
|
|
173
|
+
format: ['esm'],
|
|
174
|
+
platform: 'node',
|
|
175
|
+
target: 'es2024',
|
|
176
|
+
fixedExtension: false,
|
|
177
|
+
dts: false,
|
|
178
|
+
// Flatten entry outputs to lib/<basename>.js so package exports keep the
|
|
179
|
+
// stable paths lib/index.js + lib/invariant.js (a directory-mirroring
|
|
180
|
+
// default would emit lib/plugin/invariant.js).
|
|
181
|
+
outputOptions: { entryFileNames: '[name].js' },
|
|
182
|
+
// Cleanup happens in the build script (`rm -rf lib` before tsc): tsdown
|
|
183
|
+
// must never wipe lib/types (tsc's declaration output) mid-pipeline.
|
|
184
|
+
clean: false,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default [
|
|
188
|
+
libConfig,
|
|
189
|
+
clientConfig,
|
|
190
|
+
assetConfig('mermaid', 'src/client/asset-mermaid.ts'),
|
|
191
|
+
assetConfig('three', 'src/client/asset-three.ts'),
|
|
192
|
+
assetConfig('echarts', 'src/client/asset-echarts.ts'),
|
|
193
|
+
]
|