dekit-cli 0.1.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.
@@ -0,0 +1,69 @@
1
+ import { createServer } from "node:http";
2
+ import { join } from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+ import type { DesignConfig } from "./types.js";
5
+
6
+ export async function startEditorServer(
7
+ configRef: { current: DesignConfig },
8
+ port: number,
9
+ designPort: number
10
+ ): Promise<ReturnType<typeof createServer>> {
11
+ const editorDistDir = join(import.meta.dirname, "../dist/editor");
12
+
13
+ const server = createServer(async (req, res) => {
14
+ const url = req.url ?? "/";
15
+
16
+ // API: return design config as JSON
17
+ if (url === "/api/config") {
18
+ const config = configRef.current;
19
+ const pagePaths: Record<string, string> = {};
20
+ for (const [name, def] of Object.entries(config.pages)) {
21
+ pagePaths[name] = join(config.baseDir, def.template);
22
+ }
23
+ const payload = {
24
+ pages: Object.keys(config.pages),
25
+ pagePaths,
26
+ components: Object.keys(config.components),
27
+ designServerUrl: `http://localhost:${designPort}`,
28
+ };
29
+ res.setHeader("Content-Type", "application/json");
30
+ res.setHeader("Access-Control-Allow-Origin", "*");
31
+ res.end(JSON.stringify(payload));
32
+ return;
33
+ }
34
+
35
+ // Serve static editor files
36
+ let filePath = join(editorDistDir, url === "/" ? "index.html" : url);
37
+ try {
38
+ const content = await readFile(filePath);
39
+ const ext = filePath.split(".").pop() ?? "";
40
+ const mimeTypes: Record<string, string> = {
41
+ html: "text/html",
42
+ js: "application/javascript",
43
+ css: "text/css",
44
+ svg: "image/svg+xml",
45
+ png: "image/png",
46
+ json: "application/json",
47
+ };
48
+ res.setHeader("Content-Type", mimeTypes[ext] ?? "application/octet-stream");
49
+ res.end(content);
50
+ } catch {
51
+ // SPA fallback
52
+ try {
53
+ const index = await readFile(join(editorDistDir, "index.html"));
54
+ res.setHeader("Content-Type", "text/html");
55
+ res.end(index);
56
+ } catch {
57
+ res.statusCode = 404;
58
+ res.end("Not found");
59
+ }
60
+ }
61
+ });
62
+
63
+ return new Promise((resolve) => {
64
+ server.listen(port, () => {
65
+ console.log(` Editor server running at http://localhost:${port}`);
66
+ resolve(server);
67
+ });
68
+ });
69
+ }
@@ -0,0 +1,314 @@
1
+ export function generateInspectorScript(): string {
2
+ return `
3
+ (function() {
4
+ let overlay = null;
5
+ let selectedNode = null;
6
+ let inspectEnabled = false;
7
+ let pageName = '';
8
+
9
+ function isOverlayEl(el) {
10
+ if (!overlay) return false;
11
+ return el === overlay || overlay.contains(el);
12
+ }
13
+
14
+ function createOverlay() {
15
+ const el = document.createElement('div');
16
+ el.id = '__dekit_overlay__';
17
+ el.style.cssText = 'position:fixed;pointer-events:none;z-index:999999;display:none;';
18
+
19
+ const layers = ['margin','border','padding','content'];
20
+ const colors = {
21
+ margin: 'rgba(246,178,107,0.66)',
22
+ border: 'rgba(255,229,153,0.66)',
23
+ padding: 'rgba(147,196,125,0.55)',
24
+ content: 'rgba(111,168,220,0.66)'
25
+ };
26
+ for (const name of layers) {
27
+ const d = document.createElement('div');
28
+ d.className = '__rl_' + name;
29
+ d.style.cssText = 'position:absolute;background:' + colors[name] + ';';
30
+ el.appendChild(d);
31
+ }
32
+
33
+ const tip = document.createElement('div');
34
+ tip.className = '__rl_tooltip';
35
+ tip.style.cssText = 'position:absolute;background:rgba(50,50,50,0.92);color:#fff;font:11px/1.4 SFMono-Regular,Menlo,monospace;padding:3px 6px;border-radius:3px;white-space:nowrap;pointer-events:none;';
36
+ el.appendChild(tip);
37
+
38
+ document.body.appendChild(el);
39
+ return el;
40
+ }
41
+
42
+ function showOverlay(node) {
43
+ if (!overlay) overlay = createOverlay();
44
+ const cs = getComputedStyle(node);
45
+ const rect = node.getBoundingClientRect();
46
+
47
+ const mt = parseFloat(cs.marginTop) || 0;
48
+ const mr = parseFloat(cs.marginRight) || 0;
49
+ const mb = parseFloat(cs.marginBottom) || 0;
50
+ const ml = parseFloat(cs.marginLeft) || 0;
51
+ const bt = parseFloat(cs.borderTopWidth) || 0;
52
+ const br = parseFloat(cs.borderRightWidth) || 0;
53
+ const bb = parseFloat(cs.borderBottomWidth) || 0;
54
+ const blw = parseFloat(cs.borderLeftWidth) || 0;
55
+ const pt = parseFloat(cs.paddingTop) || 0;
56
+ const pr = parseFloat(cs.paddingRight) || 0;
57
+ const pb = parseFloat(cs.paddingBottom) || 0;
58
+ const pl = parseFloat(cs.paddingLeft) || 0;
59
+
60
+ const mw = ml + rect.width + mr;
61
+ const mh = mt + rect.height + mb;
62
+
63
+ overlay.style.display = 'block';
64
+ overlay.style.left = (rect.x - ml) + 'px';
65
+ overlay.style.top = (rect.y - mt) + 'px';
66
+ overlay.style.width = mw + 'px';
67
+ overlay.style.height = mh + 'px';
68
+
69
+ // Coordinates relative to overlay container (margin box origin)
70
+ // Border box
71
+ const bx0 = ml, by0 = mt;
72
+ const bx1 = ml + rect.width, by1 = mt + rect.height;
73
+ // Padding box
74
+ const px0 = ml + blw, py0 = mt + bt;
75
+ const px1 = ml + rect.width - br, py1 = mt + rect.height - bb;
76
+ // Content box
77
+ const cx0 = ml + blw + pl, cy0 = mt + bt + pt;
78
+ const cx1 = ml + rect.width - br - pr, cy1 = mt + rect.height - bb - pb;
79
+
80
+ function ring(ox0, oy0, ox1, oy1, ix0, iy0, ix1, iy1) {
81
+ // Two separate closed subpaths via SVG path syntax — no connecting edges
82
+ return "path(evenodd, '" +
83
+ 'M '+ox0+' '+oy0+' L '+ox1+' '+oy0+' L '+ox1+' '+oy1+' L '+ox0+' '+oy1+' Z ' +
84
+ 'M '+ix0+' '+iy0+' L '+ix1+' '+iy0+' L '+ix1+' '+iy1+' L '+ix0+' '+iy1+" Z')";
85
+ }
86
+
87
+ // Each ring layer fills the container; clip-path cuts it to only show its ring
88
+ const marginEl = overlay.querySelector('.__rl_margin');
89
+ marginEl.style.left = '0px';
90
+ marginEl.style.top = '0px';
91
+ marginEl.style.width = mw + 'px';
92
+ marginEl.style.height = mh + 'px';
93
+ marginEl.style.clipPath = ring(0, 0, mw, mh, bx0, by0, bx1, by1);
94
+
95
+ const borderEl = overlay.querySelector('.__rl_border');
96
+ borderEl.style.left = '0px';
97
+ borderEl.style.top = '0px';
98
+ borderEl.style.width = mw + 'px';
99
+ borderEl.style.height = mh + 'px';
100
+ borderEl.style.clipPath = ring(bx0, by0, bx1, by1, px0, py0, px1, py1);
101
+
102
+ const paddingEl = overlay.querySelector('.__rl_padding');
103
+ paddingEl.style.left = '0px';
104
+ paddingEl.style.top = '0px';
105
+ paddingEl.style.width = mw + 'px';
106
+ paddingEl.style.height = mh + 'px';
107
+ paddingEl.style.clipPath = ring(px0, py0, px1, py1, cx0, cy0, cx1, cy1);
108
+
109
+ const contentEl = overlay.querySelector('.__rl_content');
110
+ contentEl.style.left = cx0 + 'px';
111
+ contentEl.style.top = cy0 + 'px';
112
+ contentEl.style.width = Math.max(0, cx1 - cx0) + 'px';
113
+ contentEl.style.height = Math.max(0, cy1 - cy0) + 'px';
114
+
115
+ const tip = overlay.querySelector('.__rl_tooltip');
116
+ const tag = node.tagName.toLowerCase();
117
+ const cls = node.getAttribute('class');
118
+ const w = Math.round(rect.width);
119
+ const h = Math.round(rect.height);
120
+ let label = tag;
121
+ if (cls) label += '.' + cls.trim().split(/\\s+/).join('.');
122
+ label += ' \\u2002 ' + w + ' \\u00d7 ' + h;
123
+ tip.textContent = label;
124
+
125
+ let tipTop = -mt - tip.offsetHeight - 4;
126
+ if (rect.y - mt + tipTop < 0) {
127
+ tipTop = mh + 4;
128
+ }
129
+ let tipLeft = ml;
130
+ tip.style.left = tipLeft + 'px';
131
+ tip.style.top = tipTop + 'px';
132
+
133
+ const tipRect = tip.getBoundingClientRect();
134
+ if (tipRect.right > window.innerWidth) {
135
+ tip.style.left = (ml - (tipRect.right - window.innerWidth) - 4) + 'px';
136
+ }
137
+ }
138
+
139
+ function hideOverlay() {
140
+ if (overlay) overlay.style.display = 'none';
141
+ }
142
+
143
+ function getNodePath(node) {
144
+ const parts = [];
145
+ let current = node;
146
+ while (current && current !== document.body) {
147
+ const parent = current.parentNode;
148
+ if (!parent) break;
149
+ const children = Array.from(parent.children);
150
+ const index = children.indexOf(current);
151
+ parts.unshift(current.tagName.toLowerCase() + '[' + index + ']');
152
+ current = parent;
153
+ }
154
+ parts.unshift('body');
155
+ return parts.join('/');
156
+ }
157
+
158
+ function getNodeInfo(node) {
159
+ const computed = getComputedStyle(node);
160
+ const rect = node.getBoundingClientRect();
161
+ const attrs = {};
162
+ for (const attr of node.attributes) {
163
+ attrs[attr.name] = attr.value;
164
+ }
165
+ const styles = {};
166
+ const styleProps = [
167
+ 'width','height','padding','margin','display','position',
168
+ 'background','backgroundColor','color','border','borderRadius','opacity',
169
+ 'fontFamily','fontSize','fontWeight','lineHeight',
170
+ 'paddingTop','paddingRight','paddingBottom','paddingLeft',
171
+ 'marginTop','marginRight','marginBottom','marginLeft',
172
+ 'borderTopWidth','borderRightWidth','borderBottomWidth','borderLeftWidth'
173
+ ];
174
+ for (const prop of styleProps) {
175
+ styles[prop] = computed[prop];
176
+ }
177
+ return {
178
+ tagName: node.tagName.toLowerCase(),
179
+ path: getNodePath(node),
180
+ attributes: attrs,
181
+ computedStyles: styles,
182
+ boundingRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
183
+ };
184
+ }
185
+
186
+ function buildDomTree(node) {
187
+ if (node.nodeType !== 1) return null;
188
+ const tagName = node.tagName.toLowerCase();
189
+ if (tagName === 'script' || tagName === 'style' || node.id === '__dekit_overlay__') return null;
190
+ const children = [];
191
+ for (const child of node.children) {
192
+ const subtree = buildDomTree(child);
193
+ if (subtree) children.push(subtree);
194
+ }
195
+ const classAttr = node.getAttribute('class');
196
+ return {
197
+ tagName,
198
+ path: getNodePath(node),
199
+ className: classAttr || '',
200
+ children
201
+ };
202
+ }
203
+
204
+ function resolveNodeByPath(path) {
205
+ const parts = path.split('/').slice(1);
206
+ let current = document.body;
207
+ for (const part of parts) {
208
+ const match = part.match(/^(.+)\\[(\\d+)\\]$/);
209
+ if (!match) break;
210
+ const children = Array.from(current.children);
211
+ current = children[parseInt(match[2])];
212
+ if (!current) break;
213
+ }
214
+ return (current && current !== document.body) ? current : null;
215
+ }
216
+
217
+ document.addEventListener('click', function(e) {
218
+ if (!inspectEnabled) return;
219
+ e.preventDefault();
220
+ e.stopPropagation();
221
+ const node = e.target;
222
+ if (isOverlayEl(node)) return;
223
+ selectedNode = node;
224
+ const info = getNodeInfo(node);
225
+ window.parent.postMessage({ type: 'NODE_SELECTED', node: info }, '*');
226
+ }, true);
227
+
228
+ document.addEventListener('mouseover', function(e) {
229
+ if (!inspectEnabled) return;
230
+ const node = e.target;
231
+ if (isOverlayEl(node)) return;
232
+ showOverlay(node);
233
+ const info = getNodeInfo(node);
234
+ window.parent.postMessage({ type: 'NODE_HOVERED', node: info }, '*');
235
+ }, true);
236
+
237
+ document.addEventListener('mouseout', function(e) {
238
+ if (!inspectEnabled) return;
239
+ const related = e.relatedTarget;
240
+ if (related && isOverlayEl(related)) return;
241
+ hideOverlay();
242
+ }, true);
243
+
244
+ // Forward Alt key events to parent for inspect mode toggle
245
+ document.addEventListener('keydown', function(e) {
246
+ if (e.key === 'Alt') {
247
+ window.parent.postMessage({ type: 'INSPECT_KEY', pressed: true }, '*');
248
+ }
249
+ }, true);
250
+ document.addEventListener('keyup', function(e) {
251
+ if (e.key === 'Alt') {
252
+ window.parent.postMessage({ type: 'INSPECT_KEY', pressed: false }, '*');
253
+ }
254
+ }, true);
255
+
256
+ window.addEventListener('message', function(e) {
257
+ const msg = e.data;
258
+ if (!msg || !msg.type) return;
259
+ if (msg.type === 'GET_DOM_TREE') {
260
+ const tree = buildDomTree(document.body);
261
+ window.parent.postMessage({ type: 'DOM_TREE', tree: tree ? [tree] : [] }, '*');
262
+ } else if (msg.type === 'HIGHLIGHT_NODE') {
263
+ const node = resolveNodeByPath(msg.path);
264
+ if (node) {
265
+ selectedNode = node;
266
+ hideOverlay();
267
+ const info = getNodeInfo(node);
268
+ window.parent.postMessage({ type: 'NODE_SELECTED', node: info }, '*');
269
+ }
270
+ } else if (msg.type === 'HOVER_NODE') {
271
+ const node = resolveNodeByPath(msg.path);
272
+ if (node) {
273
+ showOverlay(node);
274
+ }
275
+ } else if (msg.type === 'CLEAR_HIGHLIGHT') {
276
+ selectedNode = null;
277
+ hideOverlay();
278
+ } else if (msg.type === 'SET_TOUCH_CURSOR') {
279
+ let touchStyle = document.getElementById('__dekit_touch_cursor__');
280
+ if (msg.enabled) {
281
+ if (!touchStyle) {
282
+ touchStyle = document.createElement('style');
283
+ touchStyle.id = '__dekit_touch_cursor__';
284
+ touchStyle.textContent = '* { cursor: none !important; }';
285
+ document.head.appendChild(touchStyle);
286
+ }
287
+ } else {
288
+ if (touchStyle) touchStyle.remove();
289
+ }
290
+ } else if (msg.type === 'SET_INSPECT_MODE') {
291
+ inspectEnabled = !!msg.enabled;
292
+ if (!inspectEnabled) {
293
+ hideOverlay();
294
+ }
295
+ } else if (msg.type === 'SET_PAGE_NAME') {
296
+ pageName = msg.page || '';
297
+ } else if (msg.type === 'COPY_FEEDBACK') {
298
+ if (overlay) {
299
+ var tip = overlay.querySelector('.__rl_tooltip');
300
+ if (tip) {
301
+ var orig = tip.textContent;
302
+ tip.textContent = '\\u2713 Copied';
303
+ tip.style.color = '#4ade80';
304
+ setTimeout(function() {
305
+ tip.textContent = orig;
306
+ tip.style.color = '#fff';
307
+ }, 800);
308
+ }
309
+ }
310
+ }
311
+ });
312
+ })();
313
+ `;
314
+ }
@@ -0,0 +1,57 @@
1
+ import type { ComponentDef } from "../types.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ function tagNameToClassName(tagName: string): string {
6
+ return tagName
7
+ .split("-")
8
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
9
+ .join("");
10
+ }
11
+
12
+ function extractTemplateContent(html: string): string {
13
+ const match = html.match(/<template[^>]*>([\s\S]*?)<\/template>/i);
14
+ return match ? match[1].trim() : html.trim();
15
+ }
16
+
17
+ export async function generateComponentRegistrationScript(
18
+ components: Record<string, ComponentDef>,
19
+ baseDir: string
20
+ ): Promise<string> {
21
+ const registrations: string[] = [];
22
+
23
+ for (const [tagName, def] of Object.entries(components)) {
24
+ const templateHtml = await readFile(join(baseDir, def.template), "utf-8");
25
+ const styleContent = await readFile(join(baseDir, def.style), "utf-8");
26
+ const innerContent = extractTemplateContent(templateHtml);
27
+ const className = tagNameToClassName(tagName);
28
+
29
+ // Escape backticks and ${} in content for use inside template literals
30
+ const escapedInnerContent = innerContent
31
+ .replace(/\\/g, "\\\\")
32
+ .replace(/`/g, "\\`")
33
+ .replace(/\$\{/g, "\\${");
34
+ const escapedStyleContent = styleContent
35
+ .replace(/\\/g, "\\\\")
36
+ .replace(/`/g, "\\`")
37
+ .replace(/\$\{/g, "\\${");
38
+
39
+ registrations.push(`
40
+ class ${className} extends HTMLElement {
41
+ constructor() {
42
+ super();
43
+ const shadow = this.attachShadow({ mode: 'open' });
44
+ const style = document.createElement('style');
45
+ style.textContent = \`${escapedStyleContent}\`;
46
+ shadow.appendChild(style);
47
+ const tpl = document.createElement('template');
48
+ tpl.innerHTML = \`${escapedInnerContent}\`;
49
+ shadow.appendChild(tpl.content.cloneNode(true));
50
+ }
51
+ }
52
+ customElements.define(${JSON.stringify(tagName)}, ${className});
53
+ `);
54
+ }
55
+
56
+ return registrations.join("\n");
57
+ }
@@ -0,0 +1,68 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { parseDesignConfig } from "./parser.js";
3
+ import { join } from "node:path";
4
+
5
+ const DESIGN_DIR = join(import.meta.dirname, "../example");
6
+
7
+ describe("parseDesignConfig", () => {
8
+ test("parses valid design.yaml", async () => {
9
+ const config = await parseDesignConfig(join(DESIGN_DIR, "design.yaml"));
10
+ expect(config.version).toBe(1.0);
11
+ expect(config.globalStyle).toBe("global.css");
12
+ expect(config.components["my-banner"]).toEqual({
13
+ template: "components/my-banner/banner.html",
14
+ style: "components/my-banner/banner.css",
15
+ });
16
+ expect(config.pages["cover"]).toEqual({
17
+ template: "pages/cover/cover.html",
18
+ style: "pages/cover/cover.css",
19
+ });
20
+ expect(config.pages["example"]).toBeDefined();
21
+ });
22
+
23
+ test("stores resolved base directory", async () => {
24
+ const config = await parseDesignConfig(join(DESIGN_DIR, "design.yaml"));
25
+ expect(config.baseDir).toBe(DESIGN_DIR);
26
+ });
27
+
28
+ test("rejects component key without hyphen", async () => {
29
+ const yaml = `
30
+ version: 1.0
31
+ global-style: "global.css"
32
+ components:
33
+ banner:
34
+ template: "components/banner/banner.html"
35
+ style: "components/banner/banner.css"
36
+ pages: {}
37
+ `;
38
+ await expect(
39
+ parseDesignConfig("/fake/design.yaml", yaml)
40
+ ).rejects.toThrow(/hyphen/i);
41
+ });
42
+
43
+ test("rejects missing template file", async () => {
44
+ const yaml = `
45
+ version: 1.0
46
+ global-style: "global.css"
47
+ components: {}
48
+ pages:
49
+ cover:
50
+ template: "pages/nonexistent.html"
51
+ style: "pages/cover/cover.css"
52
+ `;
53
+ await expect(
54
+ parseDesignConfig(join(DESIGN_DIR, "design.yaml"), yaml)
55
+ ).rejects.toThrow(/not found|does not exist/i);
56
+ });
57
+
58
+ test("rejects missing version field", async () => {
59
+ const yaml = `
60
+ global-style: "global.css"
61
+ components: {}
62
+ pages: {}
63
+ `;
64
+ await expect(
65
+ parseDesignConfig("/fake/design.yaml", yaml)
66
+ ).rejects.toThrow(/version/i);
67
+ });
68
+ });
package/src/parser.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import yaml from "js-yaml";
5
+ import type { DesignConfig, ComponentDef, PageDef } from "./types.js";
6
+
7
+ export async function parseDesignConfig(
8
+ configPath: string,
9
+ rawContent?: string
10
+ ): Promise<DesignConfig> {
11
+ const absolutePath = resolve(configPath);
12
+ const baseDir = dirname(absolutePath);
13
+ const content = rawContent ?? (await readFile(absolutePath, "utf-8"));
14
+ const doc = yaml.load(content) as Record<string, unknown>;
15
+
16
+ if (!doc || typeof doc !== "object") {
17
+ throw new Error("Invalid design.yaml: not a valid YAML object");
18
+ }
19
+
20
+ if (doc.version == null) {
21
+ throw new Error("Invalid design.yaml: missing 'version' field");
22
+ }
23
+
24
+ // Validate file paths if the base directory actually exists on disk
25
+ const shouldValidateFiles = existsSync(baseDir);
26
+
27
+ const globalStyle = (doc["global-style"] as string) ?? "";
28
+ if (globalStyle && shouldValidateFiles) {
29
+ const globalStylePath = join(baseDir, globalStyle);
30
+ if (!existsSync(globalStylePath)) {
31
+ throw new Error(
32
+ `Global style file not found: ${globalStylePath}`
33
+ );
34
+ }
35
+ }
36
+
37
+ const rawComponents =
38
+ (doc.components as Record<string, { template: string; style: string }>) ??
39
+ {};
40
+ const components: Record<string, ComponentDef> = {};
41
+ for (const [key, val] of Object.entries(rawComponents)) {
42
+ if (!key.includes("-")) {
43
+ throw new Error(
44
+ `Component key "${key}" must contain a hyphen (Web Component spec requires it)`
45
+ );
46
+ }
47
+ if (shouldValidateFiles) {
48
+ validateFilePaths(baseDir, key, val.template, val.style);
49
+ }
50
+ components[key] = { template: val.template, style: val.style };
51
+ }
52
+
53
+ const rawPages =
54
+ (doc.pages as Record<string, { template: string; style: string }>) ?? {};
55
+ const pages: Record<string, PageDef> = {};
56
+ for (const [key, val] of Object.entries(rawPages)) {
57
+ if (shouldValidateFiles) {
58
+ validateFilePaths(baseDir, key, val.template, val.style);
59
+ }
60
+ pages[key] = { template: val.template, style: val.style };
61
+ }
62
+
63
+ return {
64
+ version: doc.version as number,
65
+ globalStyle,
66
+ components,
67
+ pages,
68
+ baseDir,
69
+ };
70
+ }
71
+
72
+ function validateFilePaths(
73
+ baseDir: string,
74
+ key: string,
75
+ template: string,
76
+ style: string
77
+ ): void {
78
+ const templatePath = join(baseDir, template);
79
+ if (!existsSync(templatePath)) {
80
+ throw new Error(
81
+ `Template file not found for "${key}": ${templatePath}`
82
+ );
83
+ }
84
+ const stylePath = join(baseDir, style);
85
+ if (!existsSync(stylePath)) {
86
+ throw new Error(
87
+ `Style file not found for "${key}": ${stylePath}`
88
+ );
89
+ }
90
+ }
package/src/server.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type { DesignConfig } from "./types.js";
2
+ import { startDesignServer } from "./design-server.js";
3
+ import { startEditorServer } from "./editor-server.js";
4
+
5
+ export interface ServerOptions {
6
+ config: DesignConfig;
7
+ port: number;
8
+ designPort: number;
9
+ }
10
+
11
+ export async function startServers(options: ServerOptions) {
12
+ const configRef = { current: options.config };
13
+
14
+ console.log("\n Dekit Dev Server\n");
15
+
16
+ const designServer = await startDesignServer(configRef, options.designPort);
17
+ const editorServer = await startEditorServer(
18
+ configRef,
19
+ options.port,
20
+ options.designPort
21
+ );
22
+
23
+ const editorUrl = `http://localhost:${options.port}`;
24
+ console.log(`\n Open ${editorUrl} in your browser\n`);
25
+
26
+ // Try to open browser
27
+ try {
28
+ const open = await import("open");
29
+ await open.default(editorUrl);
30
+ } catch {
31
+ // Silently ignore if open fails
32
+ }
33
+
34
+ return { designServer, editorServer };
35
+ }
package/src/types.ts ADDED
@@ -0,0 +1,17 @@
1
+ export interface ComponentDef {
2
+ template: string;
3
+ style: string;
4
+ }
5
+
6
+ export interface PageDef {
7
+ template: string;
8
+ style: string;
9
+ }
10
+
11
+ export interface DesignConfig {
12
+ version: number;
13
+ globalStyle: string;
14
+ components: Record<string, ComponentDef>;
15
+ pages: Record<string, PageDef>;
16
+ baseDir: string; // absolute path to directory containing design.yaml
17
+ }