@jxsuite/studio 0.0.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/dist/studio.css +3676 -0
- package/dist/studio.js +188743 -0
- package/dist/studio.js.map +1448 -0
- package/package.json +67 -0
- package/src/editor/context-menu.js +144 -0
- package/src/editor/inline-edit.js +597 -0
- package/src/editor/inline-format.js +572 -0
- package/src/editor/shortcuts.js +275 -0
- package/src/editor/slash-menu.js +167 -0
- package/src/files/components.js +40 -0
- package/src/files/file-ops.js +195 -0
- package/src/files/files.js +569 -0
- package/src/markdown/md-allowlist.js +101 -0
- package/src/markdown/md-convert.js +491 -0
- package/src/panels/activity-bar.js +69 -0
- package/src/panels/data-explorer.js +181 -0
- package/src/panels/events-panel.js +235 -0
- package/src/panels/imports-panel.js +427 -0
- package/src/panels/signals-panel.js +1093 -0
- package/src/panels/statusbar.js +56 -0
- package/src/platform.js +31 -0
- package/src/platforms/devserver.js +293 -0
- package/src/services/cem-export.js +130 -0
- package/src/services/code-services.js +98 -0
- package/src/site-context.js +122 -0
- package/src/state.js +744 -0
- package/src/store.js +332 -0
- package/src/studio.js +7692 -0
- package/src/ui/icons.js +83 -0
- package/src/ui/jx-styled-combobox.js +142 -0
- package/src/ui/spectrum.js +238 -0
- package/src/utils/studio-utils.js +185 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Statusbar — status message display for Jx Studio */
|
|
2
|
+
|
|
3
|
+
import { statusbarEl, getNodeAtPath, nodeLabel } from "../store.js";
|
|
4
|
+
|
|
5
|
+
// ─── Module state ────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
let statusMsg = "";
|
|
8
|
+
/** @type {any} */
|
|
9
|
+
let statusTimeout;
|
|
10
|
+
/** @type {(() => void) | null} */
|
|
11
|
+
let _rerender = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Register the callback used to re-render the statusbar. Called once from studio.js during init.
|
|
15
|
+
*
|
|
16
|
+
* @param {() => void} fn
|
|
17
|
+
*/
|
|
18
|
+
export function setStatusbarRenderer(fn) {
|
|
19
|
+
_rerender = fn;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Statusbar ───────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Render the statusbar text. Receives current studio state so the module stays decoupled from the
|
|
26
|
+
* mutable `S` local in studio.js.
|
|
27
|
+
*
|
|
28
|
+
* @param {any} S - Current studio state
|
|
29
|
+
*/
|
|
30
|
+
export function renderStatusbar(S) {
|
|
31
|
+
const parts = [];
|
|
32
|
+
if (S.mode === "content") parts.push("Content Mode");
|
|
33
|
+
if (S.selection) {
|
|
34
|
+
const node = getNodeAtPath(S.document, S.selection);
|
|
35
|
+
parts.push(`Selected: ${nodeLabel(node)}`);
|
|
36
|
+
parts.push(`Path: ${S.selection.join(" > ") || "root"}`);
|
|
37
|
+
}
|
|
38
|
+
if (statusMsg) parts.push(statusMsg);
|
|
39
|
+
statusbarEl.textContent = parts.join(" | ") || "Jx Studio";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Show a temporary status message.
|
|
44
|
+
*
|
|
45
|
+
* @param {any} msg
|
|
46
|
+
* @param {number} [duration]
|
|
47
|
+
*/
|
|
48
|
+
export function statusMessage(msg, duration = 3000) {
|
|
49
|
+
statusMsg = msg;
|
|
50
|
+
_rerender?.();
|
|
51
|
+
clearTimeout(statusTimeout);
|
|
52
|
+
statusTimeout = setTimeout(() => {
|
|
53
|
+
statusMsg = "";
|
|
54
|
+
_rerender?.();
|
|
55
|
+
}, duration);
|
|
56
|
+
}
|
package/src/platform.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform.js — Platform Abstraction Layer (PAL)
|
|
3
|
+
*
|
|
4
|
+
* Studio is backend-agnostic. Each deployment target (desktop, dev server, cloud) registers a
|
|
5
|
+
* platform adapter at startup. All file I/O, project loading, and component discovery goes through
|
|
6
|
+
* this interface.
|
|
7
|
+
*
|
|
8
|
+
* See spec/desktop.md §3 for the full StudioPlatform interface.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @typedef {Record<string, any>} StudioPlatform */
|
|
12
|
+
|
|
13
|
+
/** @type {StudioPlatform | null} */
|
|
14
|
+
let _platform = null;
|
|
15
|
+
|
|
16
|
+
/** @param {StudioPlatform} platform */
|
|
17
|
+
export function registerPlatform(platform) {
|
|
18
|
+
_platform = platform;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @returns {StudioPlatform} */
|
|
22
|
+
export function getPlatform() {
|
|
23
|
+
if (!_platform)
|
|
24
|
+
throw new Error("No platform registered. Call registerPlatform() before starting Studio.");
|
|
25
|
+
return _platform;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @returns {boolean} */
|
|
29
|
+
export function hasPlatform() {
|
|
30
|
+
return _platform !== null;
|
|
31
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devserver.js — Dev Server Platform Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements the StudioPlatform interface for the @jxplatform/server development workflow. All file
|
|
5
|
+
* I/O goes through /__studio/* REST endpoints. Project opening uses the Chrome File System Access
|
|
6
|
+
* API (showDirectoryPicker).
|
|
7
|
+
*
|
|
8
|
+
* See spec/desktop.md §8 for the full specification.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a DevServerPlatform instance.
|
|
13
|
+
*
|
|
14
|
+
* The adapter is stateless apart from `_projectRoot`, which tracks the server-relative project
|
|
15
|
+
* directory (e.g. "examples/site-demo"). All paths passed INTO PAL methods are project-relative;
|
|
16
|
+
* the adapter prefixes them with `_projectRoot` before hitting the server, and strips the prefix
|
|
17
|
+
* from responses.
|
|
18
|
+
*/
|
|
19
|
+
export function createDevServerPlatform() {
|
|
20
|
+
let _projectRoot = ".";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Prefix a project-relative path with the active project root for server API calls.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} rel
|
|
26
|
+
*/
|
|
27
|
+
function serverPath(rel) {
|
|
28
|
+
if (!_projectRoot || _projectRoot === ".") return rel;
|
|
29
|
+
return rel === "." ? _projectRoot : `${_projectRoot}/${rel}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strip the project root prefix from a server-root-relative path.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} path
|
|
36
|
+
*/
|
|
37
|
+
function stripRoot(path) {
|
|
38
|
+
if (!_projectRoot || _projectRoot === ".") return path;
|
|
39
|
+
return path.startsWith(_projectRoot + "/") ? path.slice(_projectRoot.length + 1) : path;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: "devserver",
|
|
44
|
+
|
|
45
|
+
/** Get or set the current project root (server-relative path). */
|
|
46
|
+
get projectRoot() {
|
|
47
|
+
return _projectRoot;
|
|
48
|
+
},
|
|
49
|
+
set projectRoot(v) {
|
|
50
|
+
_projectRoot = v || ".";
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// ─── Project opening ──────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
async openProject() {
|
|
56
|
+
// Use Chrome's showDirectoryPicker API
|
|
57
|
+
if (!("showDirectoryPicker" in window)) {
|
|
58
|
+
throw new Error("showDirectoryPicker not available — use a Chromium-based browser");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let dirHandle;
|
|
62
|
+
try {
|
|
63
|
+
dirHandle = await /** @type {any} */ (window).showDirectoryPicker({ mode: "readwrite" });
|
|
64
|
+
} catch (/** @type {any} */ e) {
|
|
65
|
+
// User cancelled the picker
|
|
66
|
+
if (e.name === "AbortError") return null;
|
|
67
|
+
throw e;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Read project.json from the chosen directory
|
|
71
|
+
let siteHandle;
|
|
72
|
+
try {
|
|
73
|
+
siteHandle = await dirHandle.getFileHandle("project.json");
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("No project.json found in selected folder");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const file = await siteHandle.getFile();
|
|
79
|
+
const config = JSON.parse(await file.text());
|
|
80
|
+
|
|
81
|
+
// Resolve server-relative path by matching against known sites
|
|
82
|
+
const sitesRes = await fetch("/__studio/sites");
|
|
83
|
+
if (!sitesRes.ok) throw new Error("Failed to fetch site list from server");
|
|
84
|
+
const sites = await sitesRes.json();
|
|
85
|
+
const match = sites.find(
|
|
86
|
+
/** @param {any} s */ (s) => JSON.stringify(s.config) === JSON.stringify(config),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (!match) {
|
|
90
|
+
throw new Error("Selected project is not under the dev server root");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_projectRoot = match.path;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
config,
|
|
97
|
+
handle: {
|
|
98
|
+
root: match.path,
|
|
99
|
+
name: config.name || match.path.split("/").pop(),
|
|
100
|
+
projectConfig: config,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Probe the server root to see if it is itself a site project. Used at startup to auto-detect
|
|
107
|
+
* projects.
|
|
108
|
+
*/
|
|
109
|
+
async probeRootProject() {
|
|
110
|
+
try {
|
|
111
|
+
const [projectRes, infoRes] = await Promise.all([
|
|
112
|
+
fetch("/__studio/project"),
|
|
113
|
+
fetch("/__studio/project-info?dir=."),
|
|
114
|
+
]);
|
|
115
|
+
const meta = projectRes.ok ? await projectRes.json() : { root: ".", name: "project" };
|
|
116
|
+
const info = infoRes.ok ? await infoRes.json() : { isSiteProject: false };
|
|
117
|
+
return { meta, info };
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
// ─── File operations ──────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/** @param {string} dir */
|
|
126
|
+
async listDirectory(dir) {
|
|
127
|
+
const res = await fetch(`/__studio/files?dir=${encodeURIComponent(serverPath(dir))}`);
|
|
128
|
+
if (!res.ok) throw new Error(`Failed to list directory: ${dir}`);
|
|
129
|
+
const entries = await res.json();
|
|
130
|
+
for (const e of entries) e.path = stripRoot(e.path);
|
|
131
|
+
return entries;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** @param {string} path */
|
|
135
|
+
async readFile(path) {
|
|
136
|
+
const res = await fetch(`/__studio/file?path=${encodeURIComponent(serverPath(path))}`);
|
|
137
|
+
if (!res.ok) throw new Error(`Failed to read file: ${path}`);
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
return data.content;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} path
|
|
144
|
+
* @param {string} content
|
|
145
|
+
*/
|
|
146
|
+
async writeFile(path, content) {
|
|
147
|
+
const res = await fetch(`/__studio/file?path=${encodeURIComponent(serverPath(path))}`, {
|
|
148
|
+
method: "PUT",
|
|
149
|
+
body: content,
|
|
150
|
+
});
|
|
151
|
+
if (!res.ok) throw new Error(`Failed to write file: ${path}`);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** @param {string} path */
|
|
155
|
+
async deleteFile(path) {
|
|
156
|
+
const res = await fetch(`/__studio/file?path=${encodeURIComponent(serverPath(path))}`, {
|
|
157
|
+
method: "DELETE",
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok && res.status !== 404) throw new Error(`Failed to delete file: ${path}`);
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} from
|
|
164
|
+
* @param {string} to
|
|
165
|
+
*/
|
|
166
|
+
async renameFile(from, to) {
|
|
167
|
+
const res = await fetch("/__studio/file/rename", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json" },
|
|
170
|
+
body: JSON.stringify({ from: serverPath(from), to: serverPath(to) }),
|
|
171
|
+
});
|
|
172
|
+
if (!res.ok) throw new Error(`Failed to rename: ${from} → ${to}`);
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
/** @param {string} _path */
|
|
176
|
+
async createDirectory(_path) {
|
|
177
|
+
// The server creates directories implicitly when writing files.
|
|
178
|
+
// Write a placeholder and delete it, or rely on mkdir behavior.
|
|
179
|
+
// For now, use the writeFile + delete approach if directory creation
|
|
180
|
+
// is explicitly needed. The server's writeFile already calls mkdir().
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// ─── Component discovery ──────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/** @param {string} dir */
|
|
186
|
+
async discoverComponents(dir) {
|
|
187
|
+
const scanDir = dir || _projectRoot;
|
|
188
|
+
const url =
|
|
189
|
+
scanDir === "."
|
|
190
|
+
? "/__studio/components"
|
|
191
|
+
: `/__studio/components?dir=${encodeURIComponent(scanDir)}`;
|
|
192
|
+
const res = await fetch(url);
|
|
193
|
+
if (!res.ok) return [];
|
|
194
|
+
return await res.json();
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// ─── Package management ──────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/** @param {string} name */
|
|
200
|
+
async addPackage(name) {
|
|
201
|
+
const res = await fetch("/__studio/packages/add", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify({ name }),
|
|
205
|
+
});
|
|
206
|
+
if (!res.ok) throw new Error(await res.text());
|
|
207
|
+
return await res.json();
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/** @param {string} name */
|
|
211
|
+
async removePackage(name) {
|
|
212
|
+
const res = await fetch("/__studio/packages/remove", {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "Content-Type": "application/json" },
|
|
215
|
+
body: JSON.stringify({ name }),
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok) throw new Error(await res.text());
|
|
218
|
+
return await res.json();
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async listPackages() {
|
|
222
|
+
const res = await fetch("/__studio/packages");
|
|
223
|
+
if (!res.ok) return [];
|
|
224
|
+
return await res.json();
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
// ─── Code services (optional) ─────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {string} action
|
|
231
|
+
* @param {any} payload
|
|
232
|
+
*/
|
|
233
|
+
async codeService(action, payload) {
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(`/__studio/code/${action}`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: { "Content-Type": "application/json" },
|
|
238
|
+
body: JSON.stringify(payload),
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok) return null;
|
|
241
|
+
return await res.json();
|
|
242
|
+
} catch {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
// ─── Site context resolution ──────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Given an absolute file path, walk up to find the nearest project.json ancestor. Returns {
|
|
251
|
+
* sitePath, projectConfig } or { sitePath: null }.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} filePath — absolute system path
|
|
254
|
+
*/
|
|
255
|
+
async resolveSiteContext(filePath) {
|
|
256
|
+
const res = await fetch(`/__studio/resolve-site?path=${encodeURIComponent(filePath)}`);
|
|
257
|
+
if (!res.ok) return { sitePath: null };
|
|
258
|
+
return await res.json();
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// ─── File location ────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
/** @param {string} name */
|
|
264
|
+
async locateFile(name) {
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch("/__studio/locate", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: { "Content-Type": "application/json" },
|
|
269
|
+
body: JSON.stringify({ name }),
|
|
270
|
+
});
|
|
271
|
+
if (res.ok) return (await res.json()).path || null;
|
|
272
|
+
} catch {}
|
|
273
|
+
return null;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
// ─── Plugin schema ────────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {string} src
|
|
280
|
+
* @param {string} prototype
|
|
281
|
+
* @param {string} base
|
|
282
|
+
*/
|
|
283
|
+
async fetchPluginSchema(src, prototype, base) {
|
|
284
|
+
const params = new URLSearchParams({ src });
|
|
285
|
+
if (prototype) params.set("prototype", prototype);
|
|
286
|
+
if (base) params.set("base", base);
|
|
287
|
+
const res = await fetch(`/__studio/plugin-schema?${params}`);
|
|
288
|
+
if (!res.ok) return null;
|
|
289
|
+
const { schema } = await res.json();
|
|
290
|
+
return schema;
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Collect slot elements from the document tree. */
|
|
2
|
+
export function collectSlots(/** @type {any} */ node, /** @type {any} */ slots = []) {
|
|
3
|
+
if (node?.tagName === "slot") {
|
|
4
|
+
slots.push(node.attributes?.name || "");
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(node?.children))
|
|
7
|
+
node.children.forEach((/** @type {any} */ c) => collectSlots(c, slots));
|
|
8
|
+
return slots;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate and download a CEM 2.1.0 manifest for the current document.
|
|
13
|
+
*
|
|
14
|
+
* @param {any} S - Studio state
|
|
15
|
+
* @param {{
|
|
16
|
+
* defCategory: (d: any) => string;
|
|
17
|
+
* normParam: (p: any) => any;
|
|
18
|
+
* collectCssParts: (node: any) => any[];
|
|
19
|
+
* }} helpers
|
|
20
|
+
*/
|
|
21
|
+
export function exportCemManifest(S, helpers) {
|
|
22
|
+
const { defCategory, normParam, collectCssParts } = helpers;
|
|
23
|
+
const doc = S.document;
|
|
24
|
+
const tagName = doc.tagName;
|
|
25
|
+
if (!tagName || !tagName.includes("-")) return;
|
|
26
|
+
|
|
27
|
+
const state = doc.state || {};
|
|
28
|
+
const members = [];
|
|
29
|
+
const attributes = [];
|
|
30
|
+
const events = [];
|
|
31
|
+
const seenEvents = new Set();
|
|
32
|
+
|
|
33
|
+
for (const [key, d] of Object.entries(state)) {
|
|
34
|
+
if (key.startsWith("#")) continue; // private
|
|
35
|
+
|
|
36
|
+
const cat = defCategory(d);
|
|
37
|
+
|
|
38
|
+
if (cat === "function") {
|
|
39
|
+
members.push({
|
|
40
|
+
kind: "method",
|
|
41
|
+
name: key,
|
|
42
|
+
...(d.description ? { description: d.description } : {}),
|
|
43
|
+
...(d.parameters ? { parameters: d.parameters.map(normParam) } : {}),
|
|
44
|
+
...(d.deprecated
|
|
45
|
+
? { deprecated: typeof d.deprecated === "string" ? d.deprecated : true }
|
|
46
|
+
: {}),
|
|
47
|
+
});
|
|
48
|
+
// Collect emits
|
|
49
|
+
if (Array.isArray(d.emits)) {
|
|
50
|
+
for (const ev of d.emits) {
|
|
51
|
+
if (ev.name && !seenEvents.has(ev.name)) {
|
|
52
|
+
seenEvents.add(ev.name);
|
|
53
|
+
events.push({
|
|
54
|
+
name: ev.name,
|
|
55
|
+
...(ev.type ? { type: ev.type } : {}),
|
|
56
|
+
...(ev.description ? { description: ev.description } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (cat === "state") {
|
|
62
|
+
members.push({
|
|
63
|
+
kind: "field",
|
|
64
|
+
name: key,
|
|
65
|
+
...(d.type ? { type: { text: d.type } } : {}),
|
|
66
|
+
...(d.default !== undefined ? { default: String(d.default) } : {}),
|
|
67
|
+
...(d.description ? { description: d.description } : {}),
|
|
68
|
+
...(d.attribute ? { attribute: d.attribute } : {}),
|
|
69
|
+
...(d.reflects ? { reflects: true } : {}),
|
|
70
|
+
...(d.deprecated
|
|
71
|
+
? { deprecated: typeof d.deprecated === "string" ? d.deprecated : true }
|
|
72
|
+
: {}),
|
|
73
|
+
});
|
|
74
|
+
if (d.attribute) {
|
|
75
|
+
attributes.push({
|
|
76
|
+
name: d.attribute,
|
|
77
|
+
...(d.type ? { type: { text: d.type } } : {}),
|
|
78
|
+
fieldName: key,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Slots
|
|
85
|
+
const slotNames = collectSlots(doc);
|
|
86
|
+
const slots = slotNames.map((/** @type {any} */ name) => ({
|
|
87
|
+
name: name || "",
|
|
88
|
+
...(name ? {} : { description: "Default slot" }),
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
// CSS custom properties
|
|
92
|
+
const style = doc.style || {};
|
|
93
|
+
const cssProperties = Object.entries(style)
|
|
94
|
+
.filter(([k]) => k.startsWith("--"))
|
|
95
|
+
.map(([name, val]) => ({ name, default: String(val) }));
|
|
96
|
+
|
|
97
|
+
// CSS parts
|
|
98
|
+
const cssParts = collectCssParts(doc).map((p) => ({ name: p.name }));
|
|
99
|
+
|
|
100
|
+
const manifest = {
|
|
101
|
+
schemaVersion: "2.1.0",
|
|
102
|
+
modules: [
|
|
103
|
+
{
|
|
104
|
+
kind: "javascript-module",
|
|
105
|
+
path: "",
|
|
106
|
+
declarations: [
|
|
107
|
+
{
|
|
108
|
+
kind: "class",
|
|
109
|
+
name: tagName,
|
|
110
|
+
tagName,
|
|
111
|
+
members,
|
|
112
|
+
...(attributes.length ? { attributes } : {}),
|
|
113
|
+
...(events.length ? { events } : {}),
|
|
114
|
+
...(slots.length ? { slots } : {}),
|
|
115
|
+
...(cssProperties.length ? { cssProperties } : {}),
|
|
116
|
+
...(cssParts.length ? { cssParts } : {}),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const blob = new Blob([JSON.stringify(manifest, null, 2)], { type: "application/json" });
|
|
124
|
+
const url = URL.createObjectURL(blob);
|
|
125
|
+
const a = document.createElement("a");
|
|
126
|
+
a.href = url;
|
|
127
|
+
a.download = `${tagName}.cem.json`;
|
|
128
|
+
a.click();
|
|
129
|
+
URL.revokeObjectURL(url);
|
|
130
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** OXC code services (server-backed) */
|
|
2
|
+
|
|
3
|
+
import { getPlatform } from "../platform.js";
|
|
4
|
+
import { getNodeAtPath } from "../store.js";
|
|
5
|
+
import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {any} action
|
|
9
|
+
* @param {any} payload
|
|
10
|
+
*/
|
|
11
|
+
export async function codeService(action, payload) {
|
|
12
|
+
const platform = getPlatform();
|
|
13
|
+
if (!platform.codeService) return null;
|
|
14
|
+
return platform.codeService(action, payload);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ask the server to locate a document by filename within the project root.
|
|
19
|
+
*
|
|
20
|
+
* @param {any} name
|
|
21
|
+
*/
|
|
22
|
+
export async function locateDocument(name) {
|
|
23
|
+
const platform = getPlatform();
|
|
24
|
+
if (!platform.locateFile) return null;
|
|
25
|
+
return platform.locateFile(name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Cache of plugin schemas keyed by "$src::$prototype". */
|
|
29
|
+
export const pluginSchemaCache = new Map();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fetch and cache the schema for an external $prototype + $src module via the server.
|
|
33
|
+
*
|
|
34
|
+
* @param {any} def
|
|
35
|
+
* @param {any} state
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchPluginSchema(def, state) {
|
|
38
|
+
if (!def.$src || !def.$prototype) return null;
|
|
39
|
+
const cacheKey = `${def.$src}::${def.$prototype}`;
|
|
40
|
+
if (pluginSchemaCache.has(cacheKey)) return pluginSchemaCache.get(cacheKey);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const platform = getPlatform();
|
|
44
|
+
if (!platform.fetchPluginSchema) {
|
|
45
|
+
pluginSchemaCache.set(cacheKey, null);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const base = state.documentPath ? `${location.origin}/${state.documentPath}` : undefined;
|
|
49
|
+
const schema = await platform.fetchPluginSchema(def.$src, def.$prototype, base);
|
|
50
|
+
pluginSchemaCache.set(cacheKey, schema);
|
|
51
|
+
return schema;
|
|
52
|
+
} catch {
|
|
53
|
+
pluginSchemaCache.set(cacheKey, null);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {any} editor
|
|
60
|
+
* @param {any[]} diagnostics
|
|
61
|
+
*/
|
|
62
|
+
export function setLintMarkers(editor, diagnostics) {
|
|
63
|
+
const model = editor.getModel();
|
|
64
|
+
if (!model) return;
|
|
65
|
+
const markers = diagnostics
|
|
66
|
+
.map((d) => {
|
|
67
|
+
const label = d.labels?.[0];
|
|
68
|
+
if (!label) return null;
|
|
69
|
+
const { line, column, length } = label.span;
|
|
70
|
+
return {
|
|
71
|
+
severity:
|
|
72
|
+
d.severity === "error" ? monaco.MarkerSeverity.Error : monaco.MarkerSeverity.Warning,
|
|
73
|
+
message: d.message + (d.help ? `\n${d.help}` : ""),
|
|
74
|
+
startLineNumber: line,
|
|
75
|
+
startColumn: column,
|
|
76
|
+
endLineNumber: line,
|
|
77
|
+
endColumn: column + (length || 1),
|
|
78
|
+
code: d.url ? { value: d.code, target: monaco.Uri.parse(d.url) } : d.code,
|
|
79
|
+
source: "oxlint",
|
|
80
|
+
};
|
|
81
|
+
})
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
monaco.editor.setModelMarkers(model, "oxlint", /** @type {any} */ (markers));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {any} editing
|
|
88
|
+
* @param {any} state
|
|
89
|
+
*/
|
|
90
|
+
export function getFunctionArgs(editing, state) {
|
|
91
|
+
if (editing.type === "def") {
|
|
92
|
+
return state.document.state?.[editing.defName]?.parameters || ["state", "event"];
|
|
93
|
+
} else if (editing.type === "event") {
|
|
94
|
+
const node = getNodeAtPath(state.document, editing.path);
|
|
95
|
+
return node?.[editing.eventKey]?.parameters || ["state", "event"];
|
|
96
|
+
}
|
|
97
|
+
return ["state", "event"];
|
|
98
|
+
}
|