dsh-webui-studio 0.1.0 → 0.2.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/PRODUCT.md +11 -7
- package/README.md +69 -21
- package/README.zh-CN.md +64 -20
- package/dist/bridge.js +10 -10
- package/dist/studio.css +1 -1
- package/dist/studio.js +16691 -10202
- package/docs/bidirectional-connection-handoff.md +729 -0
- package/docs/harmony-api-requirements.md +17 -13
- package/docs/remote-development.md +80 -0
- package/lib/bridge/element-style-selector.d.ts +1 -0
- package/lib/bridge/element-style-selector.js +53 -0
- package/lib/contracts.d.ts +152 -83
- package/lib/contracts.js +0 -2
- package/lib/host/agent.d.ts +15 -13
- package/lib/host/agent.js +213 -56
- package/lib/host/automatic-patch.d.ts +9 -0
- package/lib/host/automatic-patch.js +433 -0
- package/lib/host/backend.d.ts +134 -4
- package/lib/host/backend.js +465 -114
- package/lib/host/drafts.d.ts +1 -1
- package/lib/host/drafts.js +65 -15
- package/lib/host/element-source.d.ts +9 -0
- package/lib/host/element-source.js +295 -0
- package/lib/host/mcp.d.ts +4 -0
- package/lib/host/mcp.js +97 -0
- package/lib/host/preview-draft.d.ts +22 -0
- package/lib/host/preview-draft.js +162 -0
- package/lib/host/preview-port.d.ts +8 -0
- package/lib/host/preview-port.js +32 -0
- package/lib/host/preview-worker.d.ts +65 -2
- package/lib/host/preview-worker.js +203 -76
- package/lib/host/preview.d.ts +26 -5
- package/lib/host/preview.js +130 -49
- package/lib/host/readiness.d.ts +2 -2
- package/lib/host/readiness.js +14 -17
- package/lib/host/routes.d.ts +1 -7
- package/lib/host/routes.js +41 -50
- package/lib/host/runtime-profile.d.ts +2 -1
- package/lib/host/runtime-profile.js +30 -8
- package/lib/host/source-resolution.d.ts +11 -1
- package/lib/host/source-resolution.js +69 -24
- package/lib/host/studio-service.d.ts +129 -0
- package/lib/host/studio-service.js +53 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +64 -30
- package/lib/studio-remote.d.ts +126 -0
- package/lib/studio-remote.js +188 -0
- package/lib/variable-tree.d.ts +2 -0
- package/lib/variable-tree.js +13 -0
- package/package.json +62 -25
- package/studio.patch.yml +12 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { unlink } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, join, posix } from 'node:path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
import { readProjectFile, writeProjectFile } from './project-files.js';
|
|
6
|
+
import { compileElementStyleSelector } from '../bridge/element-style-selector.js';
|
|
7
|
+
function digest(value, length = 12) {
|
|
8
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, length);
|
|
9
|
+
}
|
|
10
|
+
function identifier(value) {
|
|
11
|
+
const result = value.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
|
|
12
|
+
return result === '' ? 'target' : result.slice(0, 32);
|
|
13
|
+
}
|
|
14
|
+
function excerpt(source, start, end) {
|
|
15
|
+
const lineStart = source.lastIndexOf('\n', start - 1) + 1;
|
|
16
|
+
const nextLine = source.indexOf('\n', end);
|
|
17
|
+
const lineEnd = nextLine === -1 ? source.length : nextLine;
|
|
18
|
+
const value = source.slice(lineStart, lineEnd).trim();
|
|
19
|
+
return value.length <= 240 ? value : `${value.slice(0, 237)}...`;
|
|
20
|
+
}
|
|
21
|
+
function match(sourceFile, source, node, applicable, reason) {
|
|
22
|
+
const start = node.getStart(sourceFile);
|
|
23
|
+
const location = sourceFile.getLineAndCharacterOfPosition(start);
|
|
24
|
+
return {
|
|
25
|
+
line: location.line + 1,
|
|
26
|
+
column: location.character + 1,
|
|
27
|
+
excerpt: excerpt(source, start, node.getEnd()),
|
|
28
|
+
applicable,
|
|
29
|
+
...(reason === undefined ? {} : { reason }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function cssVariable(value) {
|
|
33
|
+
if (typeof value !== 'object' || value === null)
|
|
34
|
+
return false;
|
|
35
|
+
const variable = value;
|
|
36
|
+
return typeof variable.id === 'string' && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(variable.id)
|
|
37
|
+
&& typeof variable.label === 'string' && variable.label !== ''
|
|
38
|
+
&& typeof variable.property === 'string' && /^(?:--)?[a-zA-Z][a-zA-Z0-9-]*$/.test(variable.property)
|
|
39
|
+
&& ['color', 'length', 'number', 'enum', 'string'].includes(variable.control ?? '')
|
|
40
|
+
&& (typeof variable.value === 'string' || (typeof variable.value === 'number' && Number.isFinite(variable.value)))
|
|
41
|
+
&& (variable.options === undefined || (Array.isArray(variable.options) && variable.options.length > 0 && variable.options.every(item => typeof item === 'string')))
|
|
42
|
+
&& (variable.constraints === undefined || Object.values(variable.constraints).every(item => typeof item === 'number' && Number.isFinite(item)));
|
|
43
|
+
}
|
|
44
|
+
function validateCssRequest(request) {
|
|
45
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(request.component))
|
|
46
|
+
throw new Error('automatic CSS Patch component name is invalid');
|
|
47
|
+
if (!/\.(?:[cm]?[jt]sx?)$/.test(request.clientFile))
|
|
48
|
+
throw new Error('automatic CSS Patch client source must be a JavaScript or TypeScript file');
|
|
49
|
+
compileElementStyleSelector(request.selector, '[data-dsh-studio-root]');
|
|
50
|
+
if (request.boundary.surfaceId === '' || request.boundary.path.length === 0 || request.boundary.path.some(item => item === '')) {
|
|
51
|
+
throw new Error('automatic CSS Patch boundary is invalid');
|
|
52
|
+
}
|
|
53
|
+
if (request.targetSelector !== undefined
|
|
54
|
+
&& (request.targetSelector.trim() === '' || request.targetSelector.length > 2_000 || /[{};]/.test(request.targetSelector))) {
|
|
55
|
+
throw new Error('automatic CSS Patch target selector is invalid');
|
|
56
|
+
}
|
|
57
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(request.elementId))
|
|
58
|
+
throw new Error('automatic CSS Patch element id is invalid');
|
|
59
|
+
if (request.elementLabel.trim() === '')
|
|
60
|
+
throw new Error('automatic CSS Patch element label must not be empty');
|
|
61
|
+
if (request.variables.length === 0)
|
|
62
|
+
throw new Error('automatic CSS Patch requires at least one variable');
|
|
63
|
+
const ids = new Set();
|
|
64
|
+
const properties = new Set();
|
|
65
|
+
for (const variable of request.variables) {
|
|
66
|
+
if (!cssVariable(variable))
|
|
67
|
+
throw new Error(`automatic CSS Patch variable ${JSON.stringify(variable.id)} is invalid`);
|
|
68
|
+
if (ids.has(variable.id))
|
|
69
|
+
throw new Error(`automatic CSS Patch variable ${JSON.stringify(variable.id)} is duplicated`);
|
|
70
|
+
if (properties.has(variable.property))
|
|
71
|
+
throw new Error(`automatic CSS Patch property ${JSON.stringify(variable.property)} is duplicated`);
|
|
72
|
+
ids.add(variable.id);
|
|
73
|
+
properties.add(variable.property);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function validateContentRequest(request) {
|
|
77
|
+
if (!/\.(?:[cm]?[jt]sx?)$/.test(request.clientFile))
|
|
78
|
+
throw new Error('automatic content Patch client source must be a JavaScript or TypeScript file');
|
|
79
|
+
compileElementStyleSelector(request.selector, '[data-dsh-studio-root]');
|
|
80
|
+
if (request.boundary.surfaceId === '' || request.boundary.path.length === 0 || request.boundary.path.some(item => item === '')) {
|
|
81
|
+
throw new Error('automatic content Patch boundary is invalid');
|
|
82
|
+
}
|
|
83
|
+
if (request.targetSelector !== undefined
|
|
84
|
+
&& (request.targetSelector.trim() === '' || request.targetSelector.length > 2_000 || /[{};]/.test(request.targetSelector))) {
|
|
85
|
+
throw new Error('automatic content Patch target selector is invalid');
|
|
86
|
+
}
|
|
87
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(request.elementId))
|
|
88
|
+
throw new Error('automatic content Patch element id is invalid');
|
|
89
|
+
if (request.elementLabel.trim() === '')
|
|
90
|
+
throw new Error('automatic content Patch element label must not be empty');
|
|
91
|
+
}
|
|
92
|
+
function analyzeTarget(request, target) {
|
|
93
|
+
const sourceFile = ts.createSourceFile(target.file, target.source, ts.ScriptTarget.Latest, true);
|
|
94
|
+
let matches;
|
|
95
|
+
if (request.kind === 'replace-string') {
|
|
96
|
+
matches = [];
|
|
97
|
+
const visit = (node) => {
|
|
98
|
+
if (ts.isStringLiteral(node) && node.text === request.text)
|
|
99
|
+
matches.push(match(sourceFile, target.source, node, true));
|
|
100
|
+
ts.forEachChild(node, visit);
|
|
101
|
+
};
|
|
102
|
+
visit(sourceFile);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
matches = [];
|
|
106
|
+
const visit = (node) => {
|
|
107
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === request.component) {
|
|
108
|
+
matches.push(match(sourceFile, target.source, node, node.initializer !== undefined, node.initializer === undefined ? 'component variable declaration has no initializer' : undefined));
|
|
109
|
+
}
|
|
110
|
+
else if (ts.isFunctionDeclaration(node) && node.name?.text === request.component) {
|
|
111
|
+
matches.push(match(sourceFile, target.source, node, node.body !== undefined, node.body === undefined ? 'component function declaration has no body' : undefined));
|
|
112
|
+
}
|
|
113
|
+
ts.forEachChild(node, visit);
|
|
114
|
+
};
|
|
115
|
+
visit(sourceFile);
|
|
116
|
+
}
|
|
117
|
+
return { package: target.package, file: target.file, version: target.version, matches };
|
|
118
|
+
}
|
|
119
|
+
function patchId(request, target) {
|
|
120
|
+
return `auto-${request.kind === 'css-style' ? 'css' : 'replace'}-${identifier(target.package)}-${digest({ request, target: { package: target.package, file: target.file, version: target.version } })}`;
|
|
121
|
+
}
|
|
122
|
+
function stringPatch(request, target, id) {
|
|
123
|
+
return ` {
|
|
124
|
+
id: ${JSON.stringify(id)},
|
|
125
|
+
target: { package: ${JSON.stringify(target.package)}, version: ${JSON.stringify(target.version)}, file: ${JSON.stringify(target.file)} },
|
|
126
|
+
select: ${JSON.stringify(`StringLiteral[text=${JSON.stringify(request.text)}]`)},
|
|
127
|
+
expect: ${target.matches.length},
|
|
128
|
+
apply({ node, sourceFile, edit }) {
|
|
129
|
+
edit.overwrite(node.getStart(sourceFile), node.getEnd(), JSON.stringify(AUTO_CONTENT))
|
|
130
|
+
},
|
|
131
|
+
}`;
|
|
132
|
+
}
|
|
133
|
+
function cssPatch(request, target, id, owner) {
|
|
134
|
+
return ` component({
|
|
135
|
+
id: ${JSON.stringify(id)},
|
|
136
|
+
target: { package: ${JSON.stringify(target.package)}, version: ${JSON.stringify(target.version)}, file: ${JSON.stringify(target.file)} },
|
|
137
|
+
select: { name: ${JSON.stringify(request.component)} },
|
|
138
|
+
expect: ${target.matches.length},
|
|
139
|
+
operation: { kind: 'decorate', with: { module: ${JSON.stringify(owner)}, export: AUTO_EXPORT } },
|
|
140
|
+
})`;
|
|
141
|
+
}
|
|
142
|
+
function cssClientSource(request, owner, id, file, exportName) {
|
|
143
|
+
const definitions = request.variables.map(variable => `{
|
|
144
|
+
kind: 'variable', id: ${JSON.stringify(variable.id)}, label: ${JSON.stringify(variable.label)}, control: ${JSON.stringify(variable.control)},
|
|
145
|
+
${variable.options === undefined ? '' : `options: ${JSON.stringify(variable.options)},`}
|
|
146
|
+
${variable.constraints === undefined ? '' : `constraints: ${JSON.stringify(variable.constraints)},`}
|
|
147
|
+
defaultSource: { file: ${JSON.stringify(file)}, before: ${JSON.stringify(` ${JSON.stringify(variable.id)}: /* dsh-studio-default:${id}:${variable.id} */ `)}, after: ${JSON.stringify(',\n')} },
|
|
148
|
+
}`).join(',\n');
|
|
149
|
+
const defaults = request.variables.map(variable => ` ${JSON.stringify(variable.id)}: /* dsh-studio-default:${id}:${variable.id} */ ${JSON.stringify(variable.value)},`).join('\n');
|
|
150
|
+
const properties = JSON.stringify(request.variables.map(item => ({ id: item.id, property: item.property })));
|
|
151
|
+
const root = `[data-ui-surface=${JSON.stringify(request.boundary.surfaceId)}][data-ui-surface-path=${JSON.stringify(JSON.stringify(request.boundary.path))}]`;
|
|
152
|
+
const selector = compileElementStyleSelector(request.selector, root);
|
|
153
|
+
return `import { registerStudioElement } from 'dsh-harmony-react/studio'
|
|
154
|
+
|
|
155
|
+
const targetSelector = ${request.targetSelector === undefined ? 'undefined' : JSON.stringify(request.targetSelector)}
|
|
156
|
+
const boundarySurface = ${JSON.stringify(request.boundary.surfaceId)}
|
|
157
|
+
const boundaryPath = ${JSON.stringify(JSON.stringify(request.boundary.path))}
|
|
158
|
+
const values = {
|
|
159
|
+
${defaults}
|
|
160
|
+
}
|
|
161
|
+
const declarations = ${properties}
|
|
162
|
+
let styleElement
|
|
163
|
+
let targetObserver
|
|
164
|
+
|
|
165
|
+
function markTargets() {
|
|
166
|
+
if (targetSelector === undefined) return
|
|
167
|
+
for (const element of document.querySelectorAll(targetSelector)) {
|
|
168
|
+
if (element.hasAttribute('data-ui-surface') || element.hasAttribute('data-ui-surface-path')) continue
|
|
169
|
+
element.setAttribute('data-ui-surface', boundarySurface)
|
|
170
|
+
element.setAttribute('data-ui-surface-path', boundaryPath)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function applyStyles() {
|
|
175
|
+
if (typeof document === 'undefined') return
|
|
176
|
+
markTargets()
|
|
177
|
+
if (styleElement === undefined) {
|
|
178
|
+
styleElement = document.createElement('style')
|
|
179
|
+
styleElement.dataset.plugin = ${JSON.stringify(owner)}
|
|
180
|
+
document.head.append(styleElement)
|
|
181
|
+
}
|
|
182
|
+
styleElement.textContent = ''
|
|
183
|
+
const sheet = styleElement.sheet
|
|
184
|
+
if (sheet === null) return
|
|
185
|
+
while (sheet.cssRules.length > 0) sheet.deleteRule(0)
|
|
186
|
+
const index = sheet.insertRule(${JSON.stringify(`${selector} {}`)}, 0)
|
|
187
|
+
const rule = sheet.cssRules[index]
|
|
188
|
+
if (!(rule instanceof CSSStyleRule)) return
|
|
189
|
+
for (const declaration of declarations) rule.style.setProperty(declaration.property, String(values[declaration.id]))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const bindings = Object.fromEntries(declarations.map(declaration => [declaration.id, {
|
|
193
|
+
get: () => values[declaration.id],
|
|
194
|
+
set: value => { values[declaration.id] = value; applyStyles() },
|
|
195
|
+
}]))
|
|
196
|
+
|
|
197
|
+
registerStudioElement({
|
|
198
|
+
owner: ${JSON.stringify(owner)},
|
|
199
|
+
element: {
|
|
200
|
+
id: ${JSON.stringify(request.elementId)}, label: ${JSON.stringify(request.elementLabel)},
|
|
201
|
+
boundary: ${JSON.stringify(request.boundary)}, source: { file: ${JSON.stringify(file)} },
|
|
202
|
+
variables: [{ kind: 'group', id: 'css', label: 'CSS', children: [
|
|
203
|
+
${definitions}
|
|
204
|
+
] }],
|
|
205
|
+
},
|
|
206
|
+
bindings,
|
|
207
|
+
})
|
|
208
|
+
applyStyles()
|
|
209
|
+
if (targetSelector !== undefined) {
|
|
210
|
+
targetObserver = new MutationObserver(markTargets)
|
|
211
|
+
targetObserver.observe(document.documentElement, { childList: true, subtree: true })
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function ${exportName}(Original) {
|
|
215
|
+
return Original
|
|
216
|
+
}
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
function contentClientSource(request, owner, id, providerFile, file, exportName) {
|
|
220
|
+
const root = `[data-ui-surface=${JSON.stringify(request.boundary.surfaceId)}][data-ui-surface-path=${JSON.stringify(JSON.stringify(request.boundary.path))}]`;
|
|
221
|
+
const selector = compileElementStyleSelector(request.selector, root);
|
|
222
|
+
const variableId = `text_${id.slice('auto-content-'.length)}`;
|
|
223
|
+
const groupId = `content_${id.slice('auto-content-'.length)}`;
|
|
224
|
+
const sourceFile = request.elementSourceFile ?? file;
|
|
225
|
+
return `import { registerStudioElement } from 'dsh-harmony-react/studio'
|
|
226
|
+
|
|
227
|
+
const targetSelector = ${request.targetSelector === undefined ? 'undefined' : JSON.stringify(request.targetSelector)}
|
|
228
|
+
const boundarySurface = ${JSON.stringify(request.boundary.surfaceId)}
|
|
229
|
+
const boundaryPath = ${JSON.stringify(JSON.stringify(request.boundary.path))}
|
|
230
|
+
let value = ${JSON.stringify(request.replacement)}
|
|
231
|
+
let initialized = false
|
|
232
|
+
const listeners = new Set()
|
|
233
|
+
|
|
234
|
+
function notify() {
|
|
235
|
+
for (const listener of listeners) listener()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function markTargets() {
|
|
239
|
+
if (targetSelector === undefined) return
|
|
240
|
+
for (const element of document.querySelectorAll(targetSelector)) {
|
|
241
|
+
if (element.hasAttribute('data-ui-surface') || element.hasAttribute('data-ui-surface-path')) continue
|
|
242
|
+
element.setAttribute('data-ui-surface', boundarySurface)
|
|
243
|
+
element.setAttribute('data-ui-surface-path', boundaryPath)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function syncText() {
|
|
248
|
+
if (typeof document === 'undefined') return
|
|
249
|
+
markTargets()
|
|
250
|
+
const elements = [...document.querySelectorAll(${JSON.stringify(selector)})]
|
|
251
|
+
if (!initialized && elements[0] !== undefined) {
|
|
252
|
+
value = elements[0].textContent ?? value
|
|
253
|
+
initialized = true
|
|
254
|
+
notify()
|
|
255
|
+
}
|
|
256
|
+
for (const element of elements) if (element.textContent !== value) element.textContent = value
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
registerStudioElement({
|
|
260
|
+
owner: ${JSON.stringify(owner)},
|
|
261
|
+
element: {
|
|
262
|
+
id: ${JSON.stringify(request.elementId)}, label: ${JSON.stringify(request.elementLabel)},
|
|
263
|
+
boundary: ${JSON.stringify(request.boundary)}, source: { file: ${JSON.stringify(sourceFile)} },
|
|
264
|
+
variables: [{ kind: 'group', id: ${JSON.stringify(groupId)}, label: 'Content', children: [{
|
|
265
|
+
kind: 'variable', id: ${JSON.stringify(variableId)}, label: 'Text', control: 'string',
|
|
266
|
+
defaultSource: { file: ${JSON.stringify(providerFile)}, before: ${JSON.stringify(`const AUTO_CONTENT = /* dsh-studio-default:${id}:text */ `)}, after: ${JSON.stringify(';\n')} },
|
|
267
|
+
}] }],
|
|
268
|
+
},
|
|
269
|
+
bindings: { ${JSON.stringify(variableId)}: {
|
|
270
|
+
get: () => value,
|
|
271
|
+
set: next => { value = String(next); initialized = true; syncText(); notify() },
|
|
272
|
+
subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener) },
|
|
273
|
+
} },
|
|
274
|
+
})
|
|
275
|
+
syncText()
|
|
276
|
+
const observer = new MutationObserver(syncText)
|
|
277
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true })
|
|
278
|
+
|
|
279
|
+
export const ${exportName} = true
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
function providerSource(request, targets, owner, defaultId) {
|
|
283
|
+
const applicable = targets.filter(target => target.matches.length > 0 && target.matches.every(item => item.applicable));
|
|
284
|
+
const patchIds = applicable.map(target => patchId(request, target));
|
|
285
|
+
const declarations = applicable.map((target, index) => request.kind === 'replace-string'
|
|
286
|
+
? stringPatch(request, target, patchIds[index])
|
|
287
|
+
: cssPatch(request, target, patchIds[index], owner)).join(',\n');
|
|
288
|
+
const prefix = request.kind === 'css-style'
|
|
289
|
+
? `'use strict'\n\nconst { component } = require('dsh-harmony-react')\nconst AUTO_EXPORT = ${JSON.stringify(`DshStudioAuto${digest(request, 10)}`)}\n\n`
|
|
290
|
+
: `'use strict'\n\nconst AUTO_CONTENT = /* dsh-studio-default:${defaultId}:text */ ${JSON.stringify(request.replacement)};\n\n`;
|
|
291
|
+
return { patchIds, source: `${prefix}module.exports = [\n${declarations}\n]\n` };
|
|
292
|
+
}
|
|
293
|
+
export function analyzeAutomaticPatch(request, sources, owner) {
|
|
294
|
+
if (request.kind === 'replace-string' && request.text === '')
|
|
295
|
+
throw new Error('automatic string Patch text must not be empty');
|
|
296
|
+
if (request.targets.length === 0)
|
|
297
|
+
throw new Error('automatic Patch requires at least one target');
|
|
298
|
+
if (request.targets.length !== sources.length)
|
|
299
|
+
throw new Error('automatic Patch target sources are incomplete');
|
|
300
|
+
if (request.kind === 'css-style')
|
|
301
|
+
validateCssRequest(request);
|
|
302
|
+
else
|
|
303
|
+
validateContentRequest(request);
|
|
304
|
+
const identities = request.targets.map(target => `${target.package}\0${target.file}`);
|
|
305
|
+
if (new Set(identities).size !== identities.length)
|
|
306
|
+
throw new Error('automatic Patch targets must be unique');
|
|
307
|
+
for (let index = 0; index < sources.length; index += 1) {
|
|
308
|
+
const target = request.targets[index];
|
|
309
|
+
const source = sources[index];
|
|
310
|
+
if (source.package !== target.package || source.file !== target.file)
|
|
311
|
+
throw new Error('automatic Patch source does not match its requested target');
|
|
312
|
+
}
|
|
313
|
+
const targets = sources.map(source => analyzeTarget(request, source));
|
|
314
|
+
const file = `patch.auto-${digest({ request, owner, versions: targets.map(target => target.version) })}.cjs`;
|
|
315
|
+
const suffix = digest({ request, owner }, 10);
|
|
316
|
+
const defaultId = `auto-${request.kind === 'css-style' ? 'css' : 'content'}-${suffix}`;
|
|
317
|
+
const generated = providerSource(request, targets, owner, defaultId);
|
|
318
|
+
const extension = extname(request.clientFile);
|
|
319
|
+
const stem = basename(request.clientFile, extension);
|
|
320
|
+
const generatedFile = posix.join(dirname(request.clientFile).split('\\').join('/'), `${stem}.dsh-studio-auto-${suffix}.js`);
|
|
321
|
+
const elementId = request.kind === 'replace-string' && request.elementSourceFile !== undefined
|
|
322
|
+
? request.elementId : `${request.elementId}-${suffix}`;
|
|
323
|
+
const client = request.kind === 'css-style'
|
|
324
|
+
? {
|
|
325
|
+
file: generatedFile,
|
|
326
|
+
source: cssClientSource({ ...request, elementId }, owner, defaultId, generatedFile, `DshStudioAuto${digest(request, 10)}`),
|
|
327
|
+
export: `DshStudioAuto${digest(request, 10)}`,
|
|
328
|
+
entryFile: request.clientFile,
|
|
329
|
+
}
|
|
330
|
+
: {
|
|
331
|
+
file: generatedFile,
|
|
332
|
+
source: contentClientSource({ ...request, elementId }, owner, defaultId, file, generatedFile, `DshStudioContent${digest(request, 10)}`),
|
|
333
|
+
export: `DshStudioContent${digest(request, 10)}`,
|
|
334
|
+
entryFile: request.clientFile,
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
request,
|
|
338
|
+
targets,
|
|
339
|
+
canApply: generated.patchIds.length > 0,
|
|
340
|
+
provider: { file, source: generated.source, patchIds: generated.patchIds },
|
|
341
|
+
client,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export async function writeAutomaticPatch(root, plan) {
|
|
345
|
+
if (!plan.canApply || plan.provider.patchIds.length === 0)
|
|
346
|
+
throw new Error('automatic Patch has no matches to apply');
|
|
347
|
+
try {
|
|
348
|
+
await readProjectFile(root, plan.provider.file);
|
|
349
|
+
throw new Error(`automatic Patch provider ${JSON.stringify(plan.provider.file)} already exists`);
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
if (error.code !== 'ENOENT')
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
const manifestSource = await readProjectFile(root, 'package.json');
|
|
356
|
+
const manifest = JSON.parse(manifestSource);
|
|
357
|
+
const current = manifest.dsh?.harmony?.patches;
|
|
358
|
+
if (!Array.isArray(current) || current.some(value => typeof value !== 'string'))
|
|
359
|
+
throw new Error('Draft package.json must declare dsh.harmony.patches as an array of file paths');
|
|
360
|
+
const declaration = `./${plan.provider.file}`;
|
|
361
|
+
if (current.includes(declaration))
|
|
362
|
+
throw new Error('automatic Patch provider is already declared');
|
|
363
|
+
let clientEntrySource;
|
|
364
|
+
let clientEntryNext;
|
|
365
|
+
if (plan.client !== undefined) {
|
|
366
|
+
clientEntrySource = await readProjectFile(root, plan.client.entryFile);
|
|
367
|
+
if (clientEntrySource.includes('__ModuleLoader__.load')) {
|
|
368
|
+
throw new Error('automatic Component Patch requires the Draft client source before it is bundled');
|
|
369
|
+
}
|
|
370
|
+
const relative = posix.relative(posix.dirname(plan.client.entryFile), plan.client.file);
|
|
371
|
+
const specifier = relative.startsWith('.') ? relative : `./${relative}`;
|
|
372
|
+
const reexport = `export { ${plan.client.export} } from ${JSON.stringify(specifier)}`;
|
|
373
|
+
if (clientEntrySource.includes(reexport))
|
|
374
|
+
throw new Error('automatic Patch client export is already declared');
|
|
375
|
+
clientEntryNext = `${clientEntrySource.trimEnd()}\n\n${reexport}\n`;
|
|
376
|
+
}
|
|
377
|
+
const nextManifest = {
|
|
378
|
+
...manifest,
|
|
379
|
+
dependencies: plan.client === undefined ? manifest.dependencies : { ...manifest.dependencies, 'dsh-harmony-react': '^0.3.0' },
|
|
380
|
+
devDependencies: plan.request.kind !== 'css-style' || manifest.devDependencies?.tsdown !== '0.22.14'
|
|
381
|
+
|| manifest.dependencies?.['@tsdown/css'] !== undefined || manifest.devDependencies['@tsdown/css'] !== undefined
|
|
382
|
+
? manifest.devDependencies
|
|
383
|
+
: { ...manifest.devDependencies, '@tsdown/css': manifest.devDependencies.tsdown },
|
|
384
|
+
dsh: {
|
|
385
|
+
...manifest.dsh,
|
|
386
|
+
...(plan.client === undefined ? {} : { client: { ...manifest.dsh?.client, immediately: true } }),
|
|
387
|
+
harmony: { ...manifest.dsh?.harmony, patches: [...current, declaration] },
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
const writes = [
|
|
391
|
+
{ file: plan.provider.file, content: plan.provider.source, created: true },
|
|
392
|
+
...(plan.client === undefined ? [] : [
|
|
393
|
+
{ file: plan.client.file, content: plan.client.source, created: true },
|
|
394
|
+
{ file: plan.client.entryFile, content: clientEntryNext, original: clientEntrySource, created: false },
|
|
395
|
+
]),
|
|
396
|
+
{ file: 'package.json', content: `${JSON.stringify(nextManifest, null, 2)}\n`, original: manifestSource, created: false },
|
|
397
|
+
];
|
|
398
|
+
const written = [];
|
|
399
|
+
try {
|
|
400
|
+
for (const write of writes) {
|
|
401
|
+
if (write.created) {
|
|
402
|
+
try {
|
|
403
|
+
await readProjectFile(root, write.file);
|
|
404
|
+
throw new Error(`automatic Patch file ${JSON.stringify(write.file)} already exists`);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
if (error.code !== 'ENOENT')
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await writeProjectFile(root, write.file, write.content);
|
|
412
|
+
written.push(write);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
const rollbackErrors = [];
|
|
417
|
+
for (const write of written.reverse()) {
|
|
418
|
+
try {
|
|
419
|
+
if (write.created)
|
|
420
|
+
await unlink(join(root, write.file));
|
|
421
|
+
else
|
|
422
|
+
await writeProjectFile(root, write.file, write.original);
|
|
423
|
+
}
|
|
424
|
+
catch (rollbackError) {
|
|
425
|
+
rollbackErrors.push(rollbackError);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (rollbackErrors.length > 0)
|
|
429
|
+
throw new AggregateError([error, ...rollbackErrors], 'automatic Patch write rollback failed');
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
return { ...plan, files: writes.map(item => item.file) };
|
|
433
|
+
}
|
package/lib/host/backend.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentRegistry } from '@deepseek-ai/dsh-agent';
|
|
2
2
|
import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
|
|
3
|
-
import type {
|
|
3
|
+
import type { StudioBuildResult, StudioAgentBinding, StudioAgentContext, StudioAutomaticPatchPlan, StudioAutomaticPatchRequest, StudioAutomaticPatchWriteResult, StudioCreateDraftInput, StudioCurrentInstanceView, StudioDraftView, StudioHarmonyInspection, StudioHarmonyProfile, StudioHarmonyProfileUpdateResult, StudioHarmonyService, StudioElementStyleSource, StudioPreviewStatus, StudioPreviewUpdate, StudioProjectFile, StudioProjectState, StudioReadinessReport, StudioSourceLocation, StudioWorkspaceState } from '../contracts.js';
|
|
4
4
|
import type { StudioCommandRunner, StudioDraftRegistry } from './drafts.js';
|
|
5
5
|
import type { StudioWorkspaceStore } from './workspace.js';
|
|
6
6
|
/** Stable-Host control plane for persistent, isolated Draft Preview runtimes. */
|
|
@@ -14,12 +14,142 @@ export declare class StudioBackend {
|
|
|
14
14
|
private readonly parentOrigin;
|
|
15
15
|
private readonly controllers;
|
|
16
16
|
private readonly controllerCreations;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
private readonly current;
|
|
18
|
+
constructor(harmony: StudioHarmonyService, agents: AgentRegistry, subprocess: SubprocessRuntime, registry: StudioDraftRegistry, workspace: StudioWorkspaceStore, commands: StudioCommandRunner, parentOrigin: string, currentBridgeCapability?: string);
|
|
19
|
+
currentGet(): StudioCurrentInstanceView;
|
|
20
|
+
currentPreviewStatus(): StudioPreviewStatus;
|
|
21
|
+
currentProjectState(): StudioProjectState;
|
|
22
|
+
currentContext(): Promise<StudioAgentContext>;
|
|
23
|
+
currentHarmonyProfile(): Promise<import("dsh-harmony").HarmonyProfileView>;
|
|
24
|
+
currentHarmonyInspect(input: {
|
|
25
|
+
package?: string;
|
|
26
|
+
file?: string;
|
|
27
|
+
}): Promise<StudioHarmonyInspection>;
|
|
28
|
+
currentReadDependencySource(input: {
|
|
29
|
+
package: string;
|
|
30
|
+
file: string;
|
|
31
|
+
}): Promise<string>;
|
|
32
|
+
currentPreviewUpdate(input: StudioPreviewUpdate): StudioPreviewStatus;
|
|
33
|
+
currentResolveSource(input: {
|
|
34
|
+
source: StudioSourceLocation;
|
|
35
|
+
}): Promise<import("../contracts.js").StudioSourceCandidate>;
|
|
36
|
+
currentAgentCreate(input: {
|
|
37
|
+
agentPreset?: string;
|
|
38
|
+
}): Promise<StudioAgentBinding>;
|
|
39
|
+
currentAgentAttach(input: {
|
|
40
|
+
sessionId: string;
|
|
41
|
+
}): Promise<StudioAgentBinding>;
|
|
42
|
+
currentAgentLeave(): Promise<StudioCurrentInstanceView>;
|
|
43
|
+
draftsList(): Promise<StudioDraftView[]>;
|
|
44
|
+
draftsCreate(input: StudioCreateDraftInput): Promise<StudioDraftView>;
|
|
45
|
+
workspaceGet(): Promise<StudioWorkspaceState>;
|
|
46
|
+
workspaceUpdate(input: StudioWorkspaceState): Promise<StudioWorkspaceState>;
|
|
47
|
+
harmonyProfile(input: {
|
|
48
|
+
draftId: string;
|
|
49
|
+
}): Promise<StudioHarmonyProfile>;
|
|
50
|
+
harmonyInspect(input: {
|
|
51
|
+
draftId: string;
|
|
52
|
+
package?: string;
|
|
53
|
+
file?: string;
|
|
54
|
+
}): Promise<StudioHarmonyInspection>;
|
|
55
|
+
harmonyUpdateProfile(input: {
|
|
56
|
+
draftId: string;
|
|
57
|
+
order?: string[];
|
|
58
|
+
patchOrder?: string[];
|
|
59
|
+
disabled?: string[];
|
|
60
|
+
}): Promise<StudioHarmonyProfileUpdateResult>;
|
|
61
|
+
draftsRename(input: {
|
|
62
|
+
draftId: string;
|
|
63
|
+
label: string;
|
|
64
|
+
}): Promise<StudioDraftView>;
|
|
65
|
+
draftsExport(input: {
|
|
66
|
+
draftId: string;
|
|
67
|
+
}): Promise<StudioDraftView>;
|
|
68
|
+
draftsStart(input: {
|
|
69
|
+
draftId: string;
|
|
70
|
+
}): Promise<StudioDraftView>;
|
|
71
|
+
draftsStop(input: {
|
|
72
|
+
draftId: string;
|
|
73
|
+
}): Promise<StudioDraftView>;
|
|
74
|
+
projectState(input: {
|
|
75
|
+
draftId: string;
|
|
76
|
+
}): Promise<StudioProjectState>;
|
|
77
|
+
projectActivate(input: {
|
|
78
|
+
draftId: string;
|
|
79
|
+
graphRev: string;
|
|
80
|
+
}): Promise<StudioProjectState>;
|
|
81
|
+
projectFiles(input: {
|
|
82
|
+
draftId: string;
|
|
83
|
+
}): Promise<StudioProjectFile[]>;
|
|
84
|
+
projectReadFile(input: {
|
|
85
|
+
draftId: string;
|
|
86
|
+
path: string;
|
|
87
|
+
}): Promise<{
|
|
88
|
+
path: string;
|
|
89
|
+
content: string;
|
|
90
|
+
}>;
|
|
91
|
+
projectWriteFile(input: {
|
|
92
|
+
draftId: string;
|
|
93
|
+
path: string;
|
|
94
|
+
content: string;
|
|
95
|
+
}): Promise<{
|
|
96
|
+
path: string;
|
|
97
|
+
saved: true;
|
|
98
|
+
}>;
|
|
99
|
+
elementsStyles(input: {
|
|
100
|
+
draftId: string;
|
|
101
|
+
}): Promise<StudioElementStyleSource[]>;
|
|
102
|
+
elementsSaveSource(input: {
|
|
103
|
+
draftId: string;
|
|
104
|
+
styles: StudioElementStyleSource[];
|
|
105
|
+
}): Promise<{
|
|
106
|
+
files: string[];
|
|
107
|
+
}>;
|
|
108
|
+
patchesAnalyzeAutomatic(input: StudioAutomaticPatchRequest & {
|
|
109
|
+
draftId: string;
|
|
110
|
+
}): Promise<StudioAutomaticPatchPlan>;
|
|
111
|
+
patchesCreateAutomatic(input: StudioAutomaticPatchRequest & {
|
|
112
|
+
draftId: string;
|
|
113
|
+
}): Promise<StudioAutomaticPatchWriteResult>;
|
|
114
|
+
projectBuild(input: {
|
|
115
|
+
draftId: string;
|
|
116
|
+
}, signal: AbortSignal): Promise<StudioBuildResult>;
|
|
117
|
+
projectCancelBuild(input: {
|
|
118
|
+
draftId: string;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
canceled: boolean;
|
|
121
|
+
}>;
|
|
122
|
+
readinessInspect(input: {
|
|
123
|
+
draftId: string;
|
|
124
|
+
}): Promise<StudioReadinessReport>;
|
|
125
|
+
readinessPack(input: {
|
|
126
|
+
draftId: string;
|
|
127
|
+
}): Promise<StudioReadinessReport>;
|
|
128
|
+
previewStatus(input: {
|
|
129
|
+
draftId: string;
|
|
130
|
+
}): Promise<StudioPreviewStatus>;
|
|
131
|
+
previewUpdate(input: StudioPreviewUpdate & {
|
|
132
|
+
draftId: string;
|
|
133
|
+
}): Promise<StudioPreviewStatus>;
|
|
134
|
+
previewResolveSource(input: {
|
|
135
|
+
draftId: string;
|
|
136
|
+
source: StudioSourceLocation;
|
|
137
|
+
}): Promise<import("../contracts.js").StudioSourceCandidate>;
|
|
138
|
+
agentCreate(input: {
|
|
139
|
+
draftId: string;
|
|
140
|
+
agentPreset?: string;
|
|
141
|
+
}): Promise<StudioAgentBinding>;
|
|
142
|
+
agentAttach(input: {
|
|
143
|
+
draftId: string;
|
|
144
|
+
sessionId: string;
|
|
145
|
+
}): Promise<StudioAgentBinding>;
|
|
146
|
+
agentLeave(input: {
|
|
147
|
+
draftId: string;
|
|
148
|
+
}): Promise<StudioDraftView>;
|
|
19
149
|
dispose(): Promise<void>;
|
|
20
150
|
private list;
|
|
21
151
|
private create;
|
|
22
152
|
private controller;
|
|
23
153
|
private makeController;
|
|
24
|
-
private
|
|
154
|
+
private parsePreviewStatus;
|
|
25
155
|
}
|