lecodes-design 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.
package/dist/server.js ADDED
@@ -0,0 +1,863 @@
1
+ // src/server/index.ts
2
+ import { createServer } from "node:http";
3
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statSync, watch } from "node:fs";
4
+ import { extname, join as join3, normalize, resolve as resolve3, sep as sep3, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // src/server/designState.ts
8
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ var DEFAULT_SCREEN_SIZE = [390, 844];
11
+ var GRID_GAP = 160;
12
+ var GRID_ROWS = 2;
13
+ var readMeta = (designDir, warnings) => {
14
+ const path = join(designDir, "meta.json");
15
+ if (!existsSync(path))
16
+ return {};
17
+ try {
18
+ return JSON.parse(readFileSync(path, "utf8"));
19
+ } catch (e) {
20
+ warnings.push(`meta.json is not valid JSON (${e instanceof Error ? e.message : String(e)}) — positions/edges ignored.`);
21
+ return {};
22
+ }
23
+ };
24
+ var listScreenFiles = (designDir) => {
25
+ const dir = join(designDir, "screens");
26
+ if (!existsSync(dir))
27
+ return [];
28
+ const out = [];
29
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
30
+ if (!entry.isFile() || !entry.name.endsWith(".ts") || entry.name.startsWith("."))
31
+ continue;
32
+ out.push({ id: entry.name.slice(0, -3), file: `screens/${entry.name}` });
33
+ }
34
+ out.sort((a, b) => a.id.localeCompare(b.id));
35
+ return out;
36
+ };
37
+ var readMetaFile = (designDir) => {
38
+ const path = join(designDir, "meta.json");
39
+ if (!existsSync(path))
40
+ return {};
41
+ return JSON.parse(readFileSync(path, "utf8"));
42
+ };
43
+ var writeMetaFile = (designDir, meta) => {
44
+ writeFileSync(join(designDir, "meta.json"), JSON.stringify(meta, null, 2) + `
45
+ `);
46
+ };
47
+ var writeScreenMeta = (designDir, id, patch) => {
48
+ const meta = readMetaFile(designDir);
49
+ meta.screens ??= {};
50
+ const entry = { ...meta.screens[id] };
51
+ if (patch.pos)
52
+ entry.pos = [Math.round(patch.pos[0]), Math.round(patch.pos[1])];
53
+ if (patch.desc !== undefined) {
54
+ if (patch.desc.trim() === "")
55
+ delete entry.desc;
56
+ else
57
+ entry.desc = patch.desc.trim();
58
+ }
59
+ meta.screens[id] = entry;
60
+ writeMetaFile(designDir, meta);
61
+ };
62
+ var addEdge = (designDir, edge) => {
63
+ const meta = readMetaFile(designDir);
64
+ meta.edges ??= [];
65
+ const clean = { from: edge.from, to: edge.to };
66
+ if (edge.label?.trim())
67
+ clean.label = edge.label.trim();
68
+ if (edge.activator?.trim())
69
+ clean.activator = edge.activator.trim();
70
+ meta.edges.push(clean);
71
+ writeMetaFile(designDir, meta);
72
+ return meta.edges.length - 1;
73
+ };
74
+ var updateEdge = (designDir, index, patch) => {
75
+ const meta = readMetaFile(designDir);
76
+ const edge = meta.edges?.[index];
77
+ if (!edge)
78
+ throw new Error(`No edge at index ${index}.`);
79
+ for (const key of ["label", "activator"]) {
80
+ const value = patch[key];
81
+ if (value === undefined)
82
+ continue;
83
+ if (value.trim() === "")
84
+ delete edge[key];
85
+ else
86
+ edge[key] = value.trim();
87
+ }
88
+ writeMetaFile(designDir, meta);
89
+ };
90
+ var removeEdge = (designDir, index) => {
91
+ const meta = readMetaFile(designDir);
92
+ if (!meta.edges?.[index])
93
+ throw new Error(`No edge at index ${index}.`);
94
+ meta.edges.splice(index, 1);
95
+ writeMetaFile(designDir, meta);
96
+ };
97
+ var readDesignState = (designDir) => {
98
+ const warnings = [];
99
+ const meta = readMeta(designDir, warnings);
100
+ const files = listScreenFiles(designDir);
101
+ const placed = files.filter((f) => meta.screens?.[f.id]?.pos);
102
+ let autoX = 0;
103
+ for (const f of placed) {
104
+ const m = meta.screens[f.id];
105
+ autoX = Math.max(autoX, m.pos[0] + (m.size?.[0] ?? DEFAULT_SCREEN_SIZE[0]) + GRID_GAP);
106
+ }
107
+ let autoIndex = 0;
108
+ const screens = files.map((f) => {
109
+ const m = meta.screens?.[f.id] ?? {};
110
+ const size = m.size ?? DEFAULT_SCREEN_SIZE;
111
+ let pos = m.pos;
112
+ if (!pos) {
113
+ const col = Math.floor(autoIndex / GRID_ROWS);
114
+ const row = autoIndex % GRID_ROWS;
115
+ pos = [autoX + col * (DEFAULT_SCREEN_SIZE[0] + GRID_GAP), row * (DEFAULT_SCREEN_SIZE[1] + GRID_GAP)];
116
+ autoIndex++;
117
+ }
118
+ return { id: f.id, file: f.file, desc: m.desc, pos, size };
119
+ });
120
+ const ids = new Set(screens.map((s) => s.id));
121
+ for (const id of Object.keys(meta.screens ?? {})) {
122
+ if (!ids.has(id))
123
+ warnings.push(`meta.json describes screen "${id}" but screens/${id}.ts does not exist.`);
124
+ }
125
+ const edges = [];
126
+ for (const [index, e] of (meta.edges ?? []).entries()) {
127
+ if (!ids.has(e.from) || !ids.has(e.to)) {
128
+ warnings.push(`Edge ${e.from} → ${e.to} references a missing screen — skipped.`);
129
+ continue;
130
+ }
131
+ edges.push({ ...e, index });
132
+ }
133
+ return { screens, edges, warnings };
134
+ };
135
+
136
+ // src/server/textEdit.ts
137
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
138
+ import { resolve, sep, relative } from "node:path";
139
+ var CALL_SCAN_LIMIT = 8000;
140
+ var FALLBACK_WINDOW = 400;
141
+ var decodeEscape = (src, i) => {
142
+ const c = src[i];
143
+ switch (c) {
144
+ case "n":
145
+ return [`
146
+ `, i + 1];
147
+ case "t":
148
+ return ["\t", i + 1];
149
+ case "r":
150
+ return ["\r", i + 1];
151
+ case "b":
152
+ return ["\b", i + 1];
153
+ case "f":
154
+ return ["\f", i + 1];
155
+ case "v":
156
+ return ["\v", i + 1];
157
+ case "0":
158
+ return ["\x00", i + 1];
159
+ case "x": {
160
+ const hex = src.slice(i + 1, i + 3);
161
+ return /^[0-9a-fA-F]{2}$/.test(hex) ? [String.fromCharCode(parseInt(hex, 16)), i + 3] : [c ?? "", i + 1];
162
+ }
163
+ case "u": {
164
+ if (src[i + 1] === "{") {
165
+ const close = src.indexOf("}", i + 2);
166
+ const hex2 = close > 0 ? src.slice(i + 2, close) : "";
167
+ if (/^[0-9a-fA-F]{1,6}$/.test(hex2))
168
+ return [String.fromCodePoint(parseInt(hex2, 16)), close + 1];
169
+ return [c ?? "", i + 1];
170
+ }
171
+ const hex = src.slice(i + 1, i + 5);
172
+ return /^[0-9a-fA-F]{4}$/.test(hex) ? [String.fromCharCode(parseInt(hex, 16)), i + 5] : [c ?? "", i + 1];
173
+ }
174
+ case `
175
+ `:
176
+ return ["", i + 1];
177
+ default:
178
+ return [c ?? "", i + 1];
179
+ }
180
+ };
181
+ var parseStringLiteral = (src, start) => {
182
+ const quote = src[start];
183
+ if (quote !== '"' && quote !== "'" && quote !== "`")
184
+ return null;
185
+ let value = "";
186
+ let i = start + 1;
187
+ while (i < src.length) {
188
+ const c = src[i];
189
+ if (c === "\\") {
190
+ const [text, next] = decodeEscape(src, i + 1);
191
+ value += text;
192
+ i = next;
193
+ continue;
194
+ }
195
+ if (c === quote)
196
+ return { start, end: i + 1, value, quote };
197
+ if (quote !== "`" && (c === `
198
+ ` || c === "\r"))
199
+ return null;
200
+ if (quote === "`" && c === "$" && src[i + 1] === "{")
201
+ return null;
202
+ value += c;
203
+ i++;
204
+ }
205
+ return null;
206
+ };
207
+ var encodeStringLiteral = (text, quote) => {
208
+ let out = quote;
209
+ for (const c of text) {
210
+ if (c === "\\")
211
+ out += "\\\\";
212
+ else if (c === quote)
213
+ out += "\\" + c;
214
+ else if (quote === "`" && c === "$")
215
+ out += "\\$";
216
+ else if (c === `
217
+ `)
218
+ out += quote === "`" ? `
219
+ ` : "\\n";
220
+ else if (c === "\r")
221
+ out += "\\r";
222
+ else if (c === "\t")
223
+ out += quote === "`" ? "\t" : "\\t";
224
+ else
225
+ out += c;
226
+ }
227
+ return out + quote;
228
+ };
229
+ var offsetOf = (src, line, column) => {
230
+ let offset = 0;
231
+ for (let l = 1;l < line; l++) {
232
+ const nl = src.indexOf(`
233
+ `, offset);
234
+ if (nl === -1)
235
+ return -1;
236
+ offset = nl + 1;
237
+ }
238
+ const end = src.indexOf(`
239
+ `, offset);
240
+ const lineLength = (end === -1 ? src.length : end) - offset;
241
+ return column <= lineLength ? offset + column : -1;
242
+ };
243
+ var directCallStringArgs = (src, from) => {
244
+ let open = -1;
245
+ for (let i2 = from;i2 < Math.min(src.length, from + 120); i2++) {
246
+ const c = src[i2];
247
+ if (c === "(") {
248
+ open = i2;
249
+ break;
250
+ }
251
+ if (c === ")" || c === ";" || c === "}")
252
+ break;
253
+ }
254
+ if (open === -1)
255
+ return [];
256
+ const out = [];
257
+ let paren = 1, brace = 0, bracket = 0;
258
+ let i = open + 1;
259
+ const limit = Math.min(src.length, open + CALL_SCAN_LIMIT);
260
+ while (i < limit && paren > 0) {
261
+ const c = src[i];
262
+ if (c === '"' || c === "'" || c === "`") {
263
+ const lit = parseStringLiteral(src, i);
264
+ if (!lit)
265
+ return [];
266
+ if (paren === 1 && brace === 0 && bracket === 0)
267
+ out.push(lit);
268
+ i = lit.end;
269
+ continue;
270
+ }
271
+ if (c === "/" && src[i + 1] === "/") {
272
+ const nl = src.indexOf(`
273
+ `, i);
274
+ i = nl === -1 ? limit : nl + 1;
275
+ continue;
276
+ }
277
+ if (c === "/" && src[i + 1] === "*") {
278
+ const close = src.indexOf("*/", i + 2);
279
+ i = close === -1 ? limit : close + 2;
280
+ continue;
281
+ }
282
+ if (c === "(")
283
+ paren++;
284
+ else if (c === ")")
285
+ paren--;
286
+ else if (c === "{")
287
+ brace++;
288
+ else if (c === "}")
289
+ brace = Math.max(0, brace - 1);
290
+ else if (c === "[")
291
+ bracket++;
292
+ else if (c === "]")
293
+ bracket = Math.max(0, bracket - 1);
294
+ i++;
295
+ }
296
+ return paren === 0 ? out : [];
297
+ };
298
+ var literalsInWindow = (src, from, window) => {
299
+ const out = [];
300
+ const limit = Math.min(src.length, from + window);
301
+ let i = from;
302
+ while (i < limit) {
303
+ const c = src[i];
304
+ if (c === '"' || c === "'" || c === "`") {
305
+ const lit = parseStringLiteral(src, i);
306
+ if (lit) {
307
+ out.push(lit);
308
+ i = lit.end;
309
+ continue;
310
+ }
311
+ }
312
+ i++;
313
+ }
314
+ return out;
315
+ };
316
+ var editTextAtSite = (options) => {
317
+ const { root, designDir, frames, oldText, newText } = options;
318
+ if (!oldText)
319
+ return { ok: false, reason: "Empty text can't be located safely — edit the code directly." };
320
+ if (oldText === newText)
321
+ return { ok: true, file: "" };
322
+ let sawDynamic = false;
323
+ for (const frame of frames) {
324
+ const rel = frame.source.replace(/^[/\\]+/, "");
325
+ const abs = resolve(root, rel);
326
+ if (!abs.startsWith(resolve(designDir) + sep))
327
+ continue;
328
+ if (!existsSync2(abs))
329
+ continue;
330
+ const src = readFileSync2(abs, "utf8");
331
+ const offset = offsetOf(src, frame.line, frame.column);
332
+ if (offset < 0)
333
+ continue;
334
+ let matches = directCallStringArgs(src, offset).filter((l) => l.value === oldText);
335
+ if (matches.length === 0) {
336
+ const near = literalsInWindow(src, offset, FALLBACK_WINDOW).filter((l) => l.value === oldText);
337
+ if (near.length === 1)
338
+ matches = near;
339
+ else if (near.length === 0 && directCallStringArgs(src, offset).length > 0)
340
+ sawDynamic = true;
341
+ } else if (matches.length > 1) {
342
+ return { ok: false, reason: "Ambiguous — the same text appears more than once at this call site." };
343
+ }
344
+ const hit = matches[0];
345
+ if (!hit)
346
+ continue;
347
+ const next = src.slice(0, hit.start) + encodeStringLiteral(newText, hit.quote) + src.slice(hit.end);
348
+ writeFileSync2(abs, next);
349
+ return { ok: true, file: relative(root, abs).replace(/\\/g, "/") };
350
+ }
351
+ return {
352
+ ok: false,
353
+ reason: sawDynamic ? "This text is computed at runtime — edit it in the code." : "Couldn't locate the text in the source — edit it in the code."
354
+ };
355
+ };
356
+
357
+ // src/server/imageEdit.ts
358
+ import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
359
+ import { join as join2, posix, relative as relative2, resolve as resolve2, sep as sep2 } from "node:path";
360
+ var WINDOW = 800;
361
+ var keyOf = (root, abs) => "/" + relative2(root, abs).replace(/\\/g, "/");
362
+ var offsetOf2 = (src, line, column) => {
363
+ let offset = 0;
364
+ for (let l = 1;l < line; l++) {
365
+ const nl = src.indexOf(`
366
+ `, offset);
367
+ if (nl === -1)
368
+ return -1;
369
+ offset = nl + 1;
370
+ }
371
+ const end = src.indexOf(`
372
+ `, offset);
373
+ const lineLength = (end === -1 ? src.length : end) - offset;
374
+ return offset + Math.min(column, lineLength);
375
+ };
376
+ var findAssetCalls = (src, from, window) => {
377
+ const out = [];
378
+ const limit = Math.min(src.length, from + window);
379
+ let i = from;
380
+ while (i < limit) {
381
+ const at = src.indexOf("asset", i);
382
+ if (at === -1 || at >= limit)
383
+ break;
384
+ i = at + 5;
385
+ if (at > 0 && /[\w$.]/.test(src[at - 1]))
386
+ continue;
387
+ let j = at + 5;
388
+ while (j < src.length && /\s/.test(src[j]))
389
+ j++;
390
+ if (src[j] !== "(")
391
+ continue;
392
+ j++;
393
+ while (j < src.length && /\s/.test(src[j]))
394
+ j++;
395
+ const literal = parseStringLiteral(src, j);
396
+ if (!literal)
397
+ continue;
398
+ let k = literal.end;
399
+ while (k < src.length && /\s/.test(src[k]))
400
+ k++;
401
+ if (src[k] !== ")")
402
+ continue;
403
+ out.push({ literal });
404
+ }
405
+ return out;
406
+ };
407
+ var findImportRefs = (src) => {
408
+ const out = [];
409
+ const importRe = /\bimport\b/g;
410
+ let m;
411
+ while (m = importRe.exec(src)) {
412
+ if (m.index > 0 && /[\w$.]/.test(src[m.index - 1]))
413
+ continue;
414
+ let i = m.index + 6;
415
+ while (i < src.length && /\s/.test(src[i]))
416
+ i++;
417
+ if (src[i] === "(" || src[i] === ".")
418
+ continue;
419
+ if (src[i] !== '"' && src[i] !== "'") {
420
+ const fromRe = /\bfrom\s*(?=["'])/g;
421
+ fromRe.lastIndex = i;
422
+ const f = fromRe.exec(src);
423
+ if (!f || f.index - m.index > 600)
424
+ continue;
425
+ i = f.index + f[0].length;
426
+ }
427
+ const literal = parseStringLiteral(src, i);
428
+ if (literal)
429
+ out.push(literal);
430
+ }
431
+ return out;
432
+ };
433
+ var assetRefsIn = (src, fileKey, oldKey) => {
434
+ const out = [];
435
+ for (const call of findAssetCalls(src, 0, src.length)) {
436
+ if (resolveAssetArg(fileKey, call.literal.value) === oldKey)
437
+ out.push({ literal: call.literal, kind: "asset" });
438
+ }
439
+ for (const literal of findImportRefs(src)) {
440
+ if (resolveAssetArg(fileKey, literal.value) === oldKey)
441
+ out.push({ literal, kind: "import" });
442
+ }
443
+ return out;
444
+ };
445
+ var findLiteralsEqual = (src, from, to, value) => {
446
+ const out = [];
447
+ const limit = Math.min(src.length, to);
448
+ let i = from;
449
+ while (i < limit) {
450
+ const c = src[i];
451
+ if (c === '"' || c === "'" || c === "`") {
452
+ const literal = parseStringLiteral(src, i);
453
+ if (literal) {
454
+ if (literal.value === value)
455
+ out.push(literal);
456
+ i = literal.end;
457
+ continue;
458
+ }
459
+ }
460
+ i++;
461
+ }
462
+ return out;
463
+ };
464
+ var tsFilesUnder = (dir) => {
465
+ const out = [];
466
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
467
+ if (entry.name.startsWith(".") || entry.name === "node_modules")
468
+ continue;
469
+ const abs = join2(dir, entry.name);
470
+ if (entry.isDirectory())
471
+ out.push(...tsFilesUnder(abs));
472
+ else if (/\.tsx?$/.test(entry.name))
473
+ out.push(abs);
474
+ }
475
+ return out;
476
+ };
477
+ var resolveAssetArg = (fileKey, arg) => {
478
+ if (arg.startsWith("/"))
479
+ return posix.normalize(arg);
480
+ if (arg.startsWith("."))
481
+ return posix.normalize(posix.join(posix.dirname(fileKey), arg));
482
+ return null;
483
+ };
484
+ var relativeAssetArg = (fileKey, assetKey) => {
485
+ const rel = posix.relative(posix.dirname(fileKey), assetKey);
486
+ return rel.startsWith(".") ? rel : `./${rel}`;
487
+ };
488
+ var sanitizeName = (name) => {
489
+ const base = name.replace(/^.*[/\\]/, "").replace(/[^\w.\- ]+/g, "-").replace(/\s+/g, "-");
490
+ return base && base !== "." && base !== ".." ? base : "image";
491
+ };
492
+ var saveAsset = (designDir, name, data) => {
493
+ const dir = join2(designDir, "assets");
494
+ mkdirSync(dir, { recursive: true });
495
+ const safe = sanitizeName(name);
496
+ const dot = safe.lastIndexOf(".");
497
+ const stem = dot > 0 ? safe.slice(0, dot) : safe;
498
+ const ext = dot > 0 ? safe.slice(dot) : "";
499
+ for (let n = 0;n < 1000; n++) {
500
+ const candidate = join2(dir, n === 0 ? safe : `${stem}-${n}${ext}`);
501
+ if (!existsSync3(candidate)) {
502
+ writeFileSync3(candidate, data);
503
+ return candidate;
504
+ }
505
+ if (data.equals(readFileSync3(candidate)))
506
+ return candidate;
507
+ }
508
+ throw new Error("Couldn't find a free asset filename.");
509
+ };
510
+ var locate = (root, designDir, frames, oldSrc) => {
511
+ const oldKey = oldSrc.startsWith("/project/") ? posix.normalize(decodeURI(oldSrc.slice("/project".length))) : null;
512
+ const frameFiles = new Set;
513
+ for (const frame of frames) {
514
+ const rel = frame.source.replace(/^[/\\]+/, "");
515
+ const abs = resolve2(root, rel);
516
+ if (!abs.startsWith(resolve2(designDir) + sep2))
517
+ continue;
518
+ if (!existsSync3(abs))
519
+ continue;
520
+ if (frameFiles.has(abs))
521
+ continue;
522
+ frameFiles.add(abs);
523
+ const src = readFileSync3(abs, "utf8");
524
+ const offset = offsetOf2(src, frame.line, frame.column);
525
+ if (offset < 0)
526
+ continue;
527
+ const fileKey = keyOf(root, abs);
528
+ if (oldKey) {
529
+ const near = findAssetCalls(src, offset, WINDOW).filter((c) => resolveAssetArg(fileKey, c.literal.value) === oldKey);
530
+ if (near.length === 1)
531
+ return { abs, fileKey, src, literal: near[0].literal, kind: "asset" };
532
+ if (near.length > 1)
533
+ return { reason: "Ambiguous — several asset() calls here point at the same file." };
534
+ const inFile = assetRefsIn(src, fileKey, oldKey);
535
+ if (inFile.length === 1)
536
+ return { abs, fileKey, src, literal: inFile[0].literal, kind: inFile[0].kind };
537
+ if (inFile.length > 1)
538
+ return { reason: "Ambiguous — this file references the same image more than once." };
539
+ } else {
540
+ const near = findLiteralsEqual(src, offset, offset + WINDOW, oldSrc);
541
+ if (near.length === 1)
542
+ return { abs, fileKey, src, literal: near[0], kind: "url" };
543
+ if (near.length > 1)
544
+ return { reason: "Ambiguous — the same source URL appears more than once here." };
545
+ const inFile = findLiteralsEqual(src, 0, src.length, oldSrc);
546
+ if (inFile.length === 1)
547
+ return { abs, fileKey, src, literal: inFile[0], kind: "url" };
548
+ if (inFile.length > 1)
549
+ return { reason: "Ambiguous — the same source URL appears more than once in this file." };
550
+ }
551
+ }
552
+ if (oldKey) {
553
+ const hits = [];
554
+ for (const abs of tsFilesUnder(designDir)) {
555
+ if (frameFiles.has(abs))
556
+ continue;
557
+ const src = readFileSync3(abs, "utf8");
558
+ const fileKey = keyOf(root, abs);
559
+ for (const ref of assetRefsIn(src, fileKey, oldKey)) {
560
+ hits.push({ abs, fileKey, src, literal: ref.literal, kind: ref.kind });
561
+ if (hits.length > 1)
562
+ return { reason: "Ambiguous — several places in the design folder reference this image." };
563
+ }
564
+ }
565
+ if (hits.length === 1)
566
+ return hits[0];
567
+ }
568
+ return { reason: "This image's source is computed at runtime — edit it in the code." };
569
+ };
570
+ var replaceImageAtSite = (options) => {
571
+ const { root, designDir, frames, oldSrc, name, data } = options;
572
+ if (!oldSrc)
573
+ return { ok: false, reason: "This image has no file source — edit the code directly." };
574
+ const target = locate(root, designDir, frames, oldSrc);
575
+ if (!("abs" in target))
576
+ return { ok: false, reason: target.reason };
577
+ const savedAbs = saveAsset(designDir, name, data);
578
+ const assetKey = keyOf(root, savedAbs);
579
+ const arg = relativeAssetArg(target.fileKey, assetKey);
580
+ const replacement = target.kind === "url" ? `asset(${encodeStringLiteral(arg, '"')})` : encodeStringLiteral(arg, target.literal.quote);
581
+ const next = target.src.slice(0, target.literal.start) + replacement + target.src.slice(target.literal.end);
582
+ writeFileSync3(target.abs, next);
583
+ return {
584
+ ok: true,
585
+ file: relative2(root, target.abs).replace(/\\/g, "/"),
586
+ path: assetKey,
587
+ src: "/project" + encodeURI(assetKey)
588
+ };
589
+ };
590
+
591
+ // src/server/index.ts
592
+ var MIME = {
593
+ ".html": "text/html; charset=utf-8",
594
+ ".js": "text/javascript; charset=utf-8",
595
+ ".mjs": "text/javascript; charset=utf-8",
596
+ ".css": "text/css; charset=utf-8",
597
+ ".json": "application/json; charset=utf-8",
598
+ ".svg": "image/svg+xml",
599
+ ".png": "image/png",
600
+ ".jpg": "image/jpeg",
601
+ ".jpeg": "image/jpeg",
602
+ ".gif": "image/gif",
603
+ ".webp": "image/webp",
604
+ ".avif": "image/avif",
605
+ ".ico": "image/x-icon",
606
+ ".wasm": "application/wasm",
607
+ ".woff": "font/woff",
608
+ ".woff2": "font/woff2",
609
+ ".ttf": "font/ttf",
610
+ ".otf": "font/otf",
611
+ ".mp3": "audio/mpeg",
612
+ ".mp4": "video/mp4",
613
+ ".txt": "text/plain; charset=utf-8"
614
+ };
615
+ var sendJson = (res, status, value) => {
616
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
617
+ res.end(JSON.stringify(value));
618
+ };
619
+ var sendFile = (res, baseDir, relPath) => {
620
+ const abs = resolve3(baseDir, "." + sep3 + normalize(relPath).replace(/^([/\\])+/, ""));
621
+ if (!abs.startsWith(resolve3(baseDir) + sep3) && abs !== resolve3(baseDir))
622
+ return false;
623
+ if (!existsSync4(abs) || !statSync(abs).isFile())
624
+ return false;
625
+ res.writeHead(200, {
626
+ "content-type": MIME[extname(abs).toLowerCase()] ?? "application/octet-stream",
627
+ "cache-control": "no-store"
628
+ });
629
+ res.end(readFileSync4(abs));
630
+ return true;
631
+ };
632
+ var startDesignServer = async (options) => {
633
+ const { root, designDir, compileScreen } = options;
634
+ const port = options.port ?? 4477;
635
+ const log = options.log ?? (() => {});
636
+ const appDir = options.appDir ?? join3(dirname(fileURLToPath(import.meta.url)), "app");
637
+ const cache = new Map;
638
+ const getCompiled = (id) => {
639
+ let entry = cache.get(id);
640
+ if (!entry) {
641
+ entry = compileScreen(id);
642
+ entry.catch(() => cache.delete(id));
643
+ cache.set(id, entry);
644
+ }
645
+ return entry;
646
+ };
647
+ const clients = new Set;
648
+ const broadcast = (event) => {
649
+ const payload = `data: ${JSON.stringify(event)}
650
+
651
+ `;
652
+ for (const res of clients)
653
+ res.write(payload);
654
+ };
655
+ let pendingScreens = new Set;
656
+ let pendingAll = false;
657
+ let pendingMeta = false;
658
+ let timer = null;
659
+ const flush = () => {
660
+ timer = null;
661
+ const event = { screens: pendingAll ? null : [...pendingScreens], meta: pendingMeta };
662
+ if (pendingAll)
663
+ cache.clear();
664
+ else
665
+ for (const id of pendingScreens)
666
+ cache.delete(id);
667
+ pendingScreens = new Set;
668
+ pendingAll = false;
669
+ pendingMeta = false;
670
+ log(`change → reload ${event.screens === null ? "all screens" : event.screens.join(", ") || "(meta only)"}`);
671
+ broadcast(event);
672
+ };
673
+ const onFsEvent = (file) => {
674
+ const key = (file ?? "").replace(/\\/g, "/");
675
+ const match = /^screens\/([^/]+)\.ts$/.exec(key);
676
+ if (match)
677
+ pendingScreens.add(match[1]);
678
+ else if (key === "meta.json")
679
+ pendingMeta = true;
680
+ else
681
+ pendingAll = true;
682
+ if (timer)
683
+ clearTimeout(timer);
684
+ timer = setTimeout(flush, 120);
685
+ };
686
+ let watcher = null;
687
+ try {
688
+ watcher = watch(designDir, { recursive: true }, (_type, file) => onFsEvent(file));
689
+ } catch {
690
+ log("fs.watch(recursive) unavailable — hot reload disabled (refresh the browser manually).");
691
+ }
692
+ const server = createServer(async (req, res) => {
693
+ const url = new URL(req.url ?? "/", "http://localhost");
694
+ const path = decodeURIComponent(url.pathname);
695
+ try {
696
+ if (path === "/api/state") {
697
+ const state = readDesignState(designDir);
698
+ return sendJson(res, 200, { name: options.name ?? "LeCodes Design", ...state });
699
+ }
700
+ const readBody = () => new Promise((resolveBody, rejectBody) => {
701
+ let data = "";
702
+ req.on("data", (chunk) => {
703
+ data += chunk;
704
+ });
705
+ req.on("end", () => {
706
+ try {
707
+ resolveBody(JSON.parse(data || "{}"));
708
+ } catch (e) {
709
+ rejectBody(e);
710
+ }
711
+ });
712
+ req.on("error", rejectBody);
713
+ });
714
+ const metaMatch = /^\/api\/screen\/([\w][\w.-]*)\/meta$/.exec(path);
715
+ if (metaMatch && req.method === "POST") {
716
+ const body = await readBody();
717
+ const pos = body.pos;
718
+ if (pos !== undefined && (!Array.isArray(pos) || pos.length !== 2 || !pos.every((n) => Number.isFinite(n)))) {
719
+ return sendJson(res, 400, { error: "pos must be [x, y]." });
720
+ }
721
+ if (body.desc !== undefined && typeof body.desc !== "string") {
722
+ return sendJson(res, 400, { error: "desc must be a string." });
723
+ }
724
+ writeScreenMeta(designDir, metaMatch[1], { pos, desc: body.desc });
725
+ return sendJson(res, 200, { ok: true });
726
+ }
727
+ if (path === "/api/edges" && req.method === "POST") {
728
+ const body = await readBody();
729
+ const ids = new Set(listScreenFiles(designDir).map((s) => s.id));
730
+ if (typeof body.from !== "string" || typeof body.to !== "string" || !ids.has(body.from) || !ids.has(body.to)) {
731
+ return sendJson(res, 400, { error: "from/to must name existing screens." });
732
+ }
733
+ const index = addEdge(designDir, {
734
+ from: body.from,
735
+ to: body.to,
736
+ label: typeof body.label === "string" ? body.label : undefined,
737
+ activator: typeof body.activator === "string" ? body.activator : undefined
738
+ });
739
+ return sendJson(res, 200, { index });
740
+ }
741
+ const edgeMatch = /^\/api\/edges\/(\d+)$/.exec(path);
742
+ if (edgeMatch && (req.method === "PUT" || req.method === "DELETE")) {
743
+ const index = Number(edgeMatch[1]);
744
+ try {
745
+ if (req.method === "DELETE") {
746
+ removeEdge(designDir, index);
747
+ } else {
748
+ const body = await readBody();
749
+ updateEdge(designDir, index, {
750
+ label: typeof body.label === "string" ? body.label : undefined,
751
+ activator: typeof body.activator === "string" ? body.activator : undefined
752
+ });
753
+ }
754
+ return sendJson(res, 200, { ok: true });
755
+ } catch (e) {
756
+ return sendJson(res, 404, { error: e instanceof Error ? e.message : String(e) });
757
+ }
758
+ }
759
+ const textMatch = /^\/api\/screen\/([\w][\w.-]*)\/text$/.exec(path);
760
+ if (textMatch && req.method === "POST") {
761
+ const body = await readBody();
762
+ const frames = Array.isArray(body.frames) ? body.frames.filter((f) => !!f && typeof f.source === "string" && Number.isInteger(f.line) && Number.isInteger(f.column)) : [];
763
+ if (frames.length === 0 || typeof body.oldText !== "string" || typeof body.newText !== "string") {
764
+ return sendJson(res, 400, { error: "frames + oldText + newText required." });
765
+ }
766
+ const result = editTextAtSite({ root, designDir, frames, oldText: body.oldText, newText: body.newText });
767
+ if (result.ok && result.file)
768
+ log(`text edit → ${result.file}`);
769
+ return sendJson(res, 200, result);
770
+ }
771
+ const imageMatch = /^\/api\/screen\/([\w][\w.-]*)\/image$/.exec(path);
772
+ if (imageMatch && req.method === "POST") {
773
+ const body = await readBody();
774
+ const frames = Array.isArray(body.frames) ? body.frames.filter((f) => !!f && typeof f.source === "string" && Number.isInteger(f.line) && Number.isInteger(f.column)) : [];
775
+ if (frames.length === 0 || typeof body.oldSrc !== "string" || typeof body.name !== "string" || typeof body.data !== "string") {
776
+ return sendJson(res, 400, { error: "frames + oldSrc + name + data (base64) required." });
777
+ }
778
+ const data = Buffer.from(body.data, "base64");
779
+ if (data.length === 0)
780
+ return sendJson(res, 400, { error: "Empty image data." });
781
+ if (data.length > 24 * 1024 * 1024)
782
+ return sendJson(res, 400, { error: "Image too large (24MB max)." });
783
+ const result = replaceImageAtSite({ root, designDir, frames, oldSrc: body.oldSrc, name: body.name, data });
784
+ if (result.ok)
785
+ log(`image → ${result.path} (${result.file})`);
786
+ return sendJson(res, 200, result);
787
+ }
788
+ const screenMatch = /^\/api\/screen\/([\w][\w.-]*)$/.exec(path);
789
+ if (screenMatch) {
790
+ const id = screenMatch[1];
791
+ if (!listScreenFiles(designDir).some((s) => s.id === id)) {
792
+ return sendJson(res, 404, { error: `No screen "${id}" (screens/${id}.ts not found).` });
793
+ }
794
+ try {
795
+ const compiled = await getCompiled(id);
796
+ return sendJson(res, 200, compiled);
797
+ } catch (e) {
798
+ return sendJson(res, 200, { js: null, warnings: [], error: e instanceof Error ? e.message : String(e) });
799
+ }
800
+ }
801
+ if (path === "/events") {
802
+ res.writeHead(200, {
803
+ "content-type": "text/event-stream",
804
+ "cache-control": "no-store",
805
+ connection: "keep-alive"
806
+ });
807
+ res.write(`retry: 1000
808
+
809
+ `);
810
+ clients.add(res);
811
+ const ping = setInterval(() => res.write(`: ping
812
+
813
+ `), 30000);
814
+ req.on("close", () => {
815
+ clients.delete(res);
816
+ clearInterval(ping);
817
+ });
818
+ return;
819
+ }
820
+ if (path.startsWith("/project/")) {
821
+ if (sendFile(res, root, path.slice("/project/".length)))
822
+ return;
823
+ res.writeHead(404);
824
+ res.end("Not found");
825
+ return;
826
+ }
827
+ if (path !== "/" && sendFile(res, appDir, path))
828
+ return;
829
+ if (!sendFile(res, appDir, "index.html")) {
830
+ res.writeHead(503, { "content-type": "text/plain" });
831
+ res.end("The design board app is not built (missing dist/app). Reinstall lecodes-design or run its build.");
832
+ }
833
+ } catch (e) {
834
+ sendJson(res, 500, { error: e instanceof Error ? e.message : String(e) });
835
+ }
836
+ });
837
+ await new Promise((resolvePromise, reject) => {
838
+ server.once("error", reject);
839
+ server.listen(port, "127.0.0.1", () => resolvePromise());
840
+ });
841
+ return {
842
+ port,
843
+ url: `http://localhost:${port}`,
844
+ close: () => {
845
+ watcher?.close();
846
+ for (const res of clients)
847
+ res.end();
848
+ clients.clear();
849
+ server.close();
850
+ }
851
+ };
852
+ };
853
+ export {
854
+ writeScreenMeta,
855
+ updateEdge,
856
+ startDesignServer,
857
+ replaceImageAtSite,
858
+ removeEdge,
859
+ readDesignState,
860
+ listScreenFiles,
861
+ editTextAtSite,
862
+ addEdge
863
+ };