framer-code-link 0.4.0 → 0.4.2

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.
Files changed (2) hide show
  1. package/dist/index.mjs +325 -119
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -5,7 +5,6 @@ import fs from "fs/promises";
5
5
  import { WebSocketServer } from "ws";
6
6
  import chokidar from "chokidar";
7
7
  import path from "path";
8
- import { getPortFromHash, isSupportedExtension, normalizePath, pluralize, sanitizeFilePath, shortProjectHash } from "@code-link/shared";
9
8
  import { createHash } from "crypto";
10
9
  import { setupTypeAcquisition } from "@typescript/ata";
11
10
  import ts from "typescript";
@@ -178,7 +177,9 @@ function debug(message, ...args) {
178
177
  function info(message, ...args) {
179
178
  if (currentLevel <= LogLevel.INFO) {
180
179
  const formatted = args.length > 0 ? `${message} ${args.join(" ")}` : message;
181
- logWithDedupe(formatted, () => console.log(formatted));
180
+ logWithDedupe(formatted, () => {
181
+ console.log(formatted);
182
+ });
182
183
  }
183
184
  }
184
185
  /**
@@ -217,19 +218,25 @@ function success(message, ...args) {
217
218
  function fileDown(fileName) {
218
219
  if (currentLevel <= LogLevel.INFO) {
219
220
  const msg = ` ${import_picocolors.default.blue("↓")} ${fileName}`;
220
- logWithDedupe(msg, () => console.log(msg));
221
+ logWithDedupe(msg, () => {
222
+ console.log(msg);
223
+ });
221
224
  }
222
225
  }
223
226
  function fileUp(fileName) {
224
227
  if (currentLevel <= LogLevel.INFO) {
225
228
  const msg = ` ${import_picocolors.default.green("↑")} ${fileName}`;
226
- logWithDedupe(msg, () => console.log(msg));
229
+ logWithDedupe(msg, () => {
230
+ console.log(msg);
231
+ });
227
232
  }
228
233
  }
229
234
  function fileDelete(fileName) {
230
235
  if (currentLevel <= LogLevel.INFO) {
231
236
  const msg = ` ${import_picocolors.default.red("×")} ${fileName}`;
232
- logWithDedupe(msg, () => console.log(msg));
237
+ logWithDedupe(msg, () => {
238
+ console.log(msg);
239
+ });
233
240
  }
234
241
  }
235
242
  /**
@@ -341,7 +348,7 @@ function initConnection(port) {
341
348
  }
342
349
  });
343
350
  ws.on("close", (code, reason) => {
344
- debug(`Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`);
351
+ debug(`Client disconnected (code: ${code}, reason: ${reason.toString()})`);
345
352
  handlers.onDisconnect?.();
346
353
  });
347
354
  ws.on("error", (err) => {
@@ -350,10 +357,20 @@ function initConnection(port) {
350
357
  });
351
358
  resolve({
352
359
  on(event, handler) {
353
- if (event === "handshake") handlers.onHandshake = handler;
354
- else if (event === "message") handlers.onMessage = handler;
355
- else if (event === "disconnect") handlers.onDisconnect = handler;
356
- else if (event === "error") handlers.onError = handler;
360
+ switch (event) {
361
+ case "handshake":
362
+ handlers.onHandshake = handler;
363
+ break;
364
+ case "message":
365
+ handlers.onMessage = handler;
366
+ break;
367
+ case "disconnect":
368
+ handlers.onDisconnect = handler;
369
+ break;
370
+ case "error":
371
+ handlers.onError = handler;
372
+ break;
373
+ }
357
374
  },
358
375
  close() {
359
376
  wss.close();
@@ -401,6 +418,173 @@ function sendMessage(socket, message) {
401
418
  });
402
419
  }
403
420
 
421
+ //#endregion
422
+ //#region ../code-link-shared/src/hash.ts
423
+ /**
424
+ * Base58 alphabet (no 0/O/I/l to avoid confusion)
425
+ */
426
+ const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
427
+ /**
428
+ * Derive a short, deterministic hash from the full Framer project hash.
429
+ * Uses a simple numeric hash encoded in base58 for compactness.
430
+ * Idempotent: if input is already the target length, returns it unchanged.
431
+ */
432
+ function shortProjectHash(fullHash, length = 8) {
433
+ if (fullHash.length === length) return fullHash;
434
+ let h1 = 0;
435
+ let h2 = 0;
436
+ for (let i = 0; i < fullHash.length; i++) {
437
+ const char = fullHash.charCodeAt(i);
438
+ h1 = Math.imul(h1 ^ char, 2246822507);
439
+ h2 = Math.imul(h2 ^ char, 3266489909);
440
+ }
441
+ h1 ^= h2 >>> 16;
442
+ h2 ^= h1 >>> 13;
443
+ let result = "";
444
+ const combined = [Math.abs(h1), Math.abs(h2)];
445
+ for (const num of combined) {
446
+ let n = num >>> 0;
447
+ while (n > 0 && result.length < length) {
448
+ result += BASE58[n % 58];
449
+ n = Math.floor(n / 58);
450
+ }
451
+ }
452
+ while (result.length < length) result += BASE58[0];
453
+ return result.slice(0, length);
454
+ }
455
+
456
+ //#endregion
457
+ //#region ../code-link-shared/src/ports.ts
458
+ /**
459
+ * Generate a deterministic port number from a project hash (full or short).
460
+ * Port range: 3847-4096 (250 possible ports)
461
+ * Must match between CLI and plugin.
462
+ *
463
+ * Internally normalizes to the short id so both full and short inputs yield the same port.
464
+ */
465
+ function getPortFromHash(projectHash) {
466
+ const shortId = shortProjectHash(projectHash);
467
+ let hash = 0;
468
+ for (let i = 0; i < shortId.length; i++) {
469
+ const char = shortId.charCodeAt(i);
470
+ hash = (hash << 5) - hash + char;
471
+ hash = hash & hash;
472
+ }
473
+ return 3847 + Math.abs(hash) % 250;
474
+ }
475
+
476
+ //#endregion
477
+ //#region ../code-link-shared/src/paths.ts
478
+ /**
479
+ * File path normalization utilities
480
+ * Framer code files include extensions in their paths (.tsx, .ts, etc.)
481
+ */
482
+ const firstCharacterRegex = /^[a-zA-Z$_]/;
483
+ const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
484
+ const onlyDotsRegex = /^\.+$/;
485
+ const tsxExtension = ".tsx";
486
+ var NameType = /* @__PURE__ */ function(NameType$1) {
487
+ NameType$1["Variable"] = "Variable";
488
+ NameType$1["Selector"] = "Selector";
489
+ NameType$1["Directory"] = "Directory";
490
+ return NameType$1;
491
+ }(NameType || {});
492
+ function sanitizedName(type, name) {
493
+ if (!name) return null;
494
+ let validName = name.trim();
495
+ if (validName.length === 0) return null;
496
+ const validFirstChar = type === NameType.Selector ? "_" : "$";
497
+ if (type === NameType.Directory) {
498
+ if (onlyDotsRegex.test(validName)) return null;
499
+ } else if (!firstCharacterRegex.test(validName)) validName = validFirstChar + validName;
500
+ validName = validName.replace(remainingCharactersRegex, "_");
501
+ validName = validName.replace(/_+/g, "_");
502
+ validName = validName.replace(/^\$_/u, validFirstChar);
503
+ return validName;
504
+ }
505
+ function sanitizedVariableName(name) {
506
+ return sanitizedName(NameType.Variable, name);
507
+ }
508
+ function sanitizedDirectoryName(name) {
509
+ return sanitizedName(NameType.Directory, name);
510
+ }
511
+ function capitalizeFirstLetter(str) {
512
+ if (str.length === 0) return str;
513
+ return str.charAt(0).toUpperCase() + str.slice(1);
514
+ }
515
+ function hasValidExtension(fileName) {
516
+ if (fileName.endsWith(".json")) return true;
517
+ return /\.[tj]sx?$/u.test(fileName);
518
+ }
519
+ function splitExtension(fileName) {
520
+ const lastDot = fileName.lastIndexOf(".");
521
+ if (lastDot <= 0) return [fileName, ""];
522
+ return [fileName.slice(0, lastDot), fileName.slice(lastDot + 1)];
523
+ }
524
+ function dirname(filePath) {
525
+ const at = filePath.lastIndexOf("/");
526
+ if (at < 0) return "";
527
+ return filePath.slice(0, at);
528
+ }
529
+ function filename(filePath) {
530
+ const at = filePath.lastIndexOf("/") + 1;
531
+ return filePath.slice(at);
532
+ }
533
+ function pathJoin(...parts) {
534
+ let res = "";
535
+ parts.forEach((part) => {
536
+ while (part.startsWith("/")) part = part.slice(1);
537
+ while (part.endsWith("/")) part = part.slice(0, -1);
538
+ if (part === "") return;
539
+ if (res !== "") res += "/";
540
+ res += part;
541
+ });
542
+ return res;
543
+ }
544
+ function normalizePath(filePath) {
545
+ if (!filePath) return "";
546
+ const isAbsolute = filePath.startsWith("/");
547
+ const segments = filePath.replace(/\\/g, "/").split("/");
548
+ const stack = [];
549
+ for (const segment of segments) {
550
+ if (!segment || segment === ".") continue;
551
+ if (segment === "..") {
552
+ if (stack.length > 0) stack.pop();
553
+ continue;
554
+ }
555
+ stack.push(segment);
556
+ }
557
+ const normalized = stack.join("/");
558
+ if (isAbsolute) return `/${normalized}`;
559
+ return normalized;
560
+ }
561
+ function sanitizeFilePath(input, capitalizeReactComponent = true) {
562
+ const trimmed = input.trim();
563
+ const [inputName, extension] = splitExtension(filename(trimmed));
564
+ const extensionWithDot = extension ? `.${extension}` : "";
565
+ const dirName = dirname(trimmed).split("/").map((part) => sanitizedDirectoryName(part)).filter((part) => Boolean(part)).join("/");
566
+ let name = sanitizedVariableName(inputName) ?? "MyComponent";
567
+ if ((!hasValidExtension(extension) || extension === tsxExtension) && capitalizeReactComponent) name = capitalizeFirstLetter(name);
568
+ return {
569
+ path: pathJoin(dirName, name + extensionWithDot),
570
+ dirName,
571
+ name,
572
+ extension
573
+ };
574
+ }
575
+ function isSupportedExtension$1(filePath) {
576
+ return /\.(tsx?|jsx?|json)$/i.test(filePath);
577
+ }
578
+ /**
579
+ * Pluralize a word based on count
580
+ * @example pluralize(1, "file") => "1 file"
581
+ * @example pluralize(3, "file") => "3 files"
582
+ * @example pluralize(0, "conflict") => "0 conflicts"
583
+ */
584
+ function pluralize(count, singular, plural) {
585
+ return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
586
+ }
587
+
404
588
  //#endregion
405
589
  //#region src/utils/node-paths.ts
406
590
  /**
@@ -449,13 +633,13 @@ function normalizePath$1(filePath) {
449
633
  function initWatcher(filesDir) {
450
634
  const handlers = [];
451
635
  const watcher = chokidar.watch(filesDir, {
452
- ignored: /(^|[\/\\])\../,
636
+ ignored: /(^|[/\\])\.\./,
453
637
  persistent: true,
454
638
  ignoreInitial: false
455
639
  });
456
640
  debug(`Watching directory: ${filesDir}`);
457
641
  const emitEvent = async (kind, absolutePath) => {
458
- if (!isSupportedExtension(absolutePath)) return;
642
+ if (!isSupportedExtension$1(absolutePath)) return;
459
643
  const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
460
644
  const relativePath = sanitizeFilePath(rawRelativePath, false).path;
461
645
  let effectiveAbsolutePath = absolutePath;
@@ -485,12 +669,18 @@ function initWatcher(filesDir) {
485
669
  debug(`Watcher event: ${kind} ${relativePath}`);
486
670
  for (const handler of handlers) handler(event);
487
671
  };
488
- watcher.on("add", (filePath) => emitEvent("add", filePath));
489
- watcher.on("change", (filePath) => emitEvent("change", filePath));
490
- watcher.on("unlink", (filePath) => emitEvent("delete", filePath));
672
+ watcher.on("add", (filePath) => {
673
+ emitEvent("add", filePath);
674
+ });
675
+ watcher.on("change", (filePath) => {
676
+ emitEvent("change", filePath);
677
+ });
678
+ watcher.on("unlink", (filePath) => {
679
+ emitEvent("delete", filePath);
680
+ });
491
681
  return {
492
- on(event, handler) {
493
- if (event === "change") handlers.push(handler);
682
+ on(_event, handler) {
683
+ handlers.push(handler);
494
684
  },
495
685
  async close() {
496
686
  await watcher.close();
@@ -613,7 +803,7 @@ async function listFiles(filesDir) {
613
803
  await walk(entryPath);
614
804
  continue;
615
805
  }
616
- if (!isSupportedExtension$1(entry.name)) continue;
806
+ if (!isSupportedExtension(entry.name)) continue;
617
807
  const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
618
808
  try {
619
809
  const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
@@ -643,7 +833,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
643
833
  const preferRemote = options.preferRemote ?? false;
644
834
  const persistedState = options.persistedState;
645
835
  const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
646
- debug(`Detecting conflicts for ${remoteFiles.length} remote files`);
836
+ debug(`Detecting conflicts for ${String(remoteFiles.length)} remote files`);
647
837
  const localFiles = await listFiles(filesDir);
648
838
  const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
649
839
  const remoteFileMap = new Map(remoteFiles.map((f) => {
@@ -666,7 +856,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
666
856
  localContent: null,
667
857
  remoteContent: remote.content,
668
858
  remoteModifiedAt: remote.modifiedAt,
669
- lastSyncedAt: persisted?.timestamp
859
+ lastSyncedAt: persisted.timestamp
670
860
  });
671
861
  } else writes.push({
672
862
  name: normalized.relativePath,
@@ -708,13 +898,13 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
708
898
  const persisted = getPersistedState(local.name);
709
899
  if (persisted) {
710
900
  const localClean = hashFileContent(local.content) === persisted.contentHash;
711
- debug(`Conflict: ${local.name} deleted in Framer (localClean=${localClean})`);
901
+ debug(`Conflict: ${local.name} deleted in Framer (localClean=${String(localClean)})`);
712
902
  conflicts.push({
713
903
  fileName: local.name,
714
904
  localContent: local.content,
715
905
  remoteContent: null,
716
906
  localModifiedAt: local.modifiedAt,
717
- lastSyncedAt: persisted?.timestamp,
907
+ lastSyncedAt: persisted.timestamp,
718
908
  localClean
719
909
  });
720
910
  } else localOnly.push({
@@ -818,7 +1008,7 @@ async function deleteLocalFile(fileName, filesDir, hashTracker) {
818
1008
  hashTracker.forget(normalized.relativePath);
819
1009
  debug(`Deleted file: ${normalized.relativePath}`);
820
1010
  } catch (err) {
821
- if (err?.code === "ENOENT") {
1011
+ if (err.code === "ENOENT") {
822
1012
  hashTracker.forget(normalized.relativePath);
823
1013
  debug(`File already deleted: ${normalized.relativePath}`);
824
1014
  return;
@@ -844,7 +1034,6 @@ async function readFileSafe(fileName, filesDir) {
844
1034
  */
845
1035
  function filterEchoedFiles(files, hashTracker) {
846
1036
  return files.filter((file) => {
847
- if (file.content === void 0) return true;
848
1037
  return !hashTracker.shouldSkip(file.name, file.content);
849
1038
  });
850
1039
  }
@@ -865,7 +1054,7 @@ function sanitizeRelativePath(relativePath) {
865
1054
  extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
866
1055
  };
867
1056
  }
868
- function isSupportedExtension$1(fileName) {
1057
+ function isSupportedExtension(fileName) {
869
1058
  const lower = fileName.toLowerCase();
870
1059
  return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
871
1060
  }
@@ -878,7 +1067,7 @@ function isSupportedExtension$1(fileName) {
878
1067
  function extractImports(code) {
879
1068
  const imports = [];
880
1069
  const seen = /* @__PURE__ */ new Set();
881
- const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([^.\/][^'"]+)['"]/g;
1070
+ const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([^./][^'"]+)['"]/g;
882
1071
  const urlRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]https?:\/\/[^'"]+['"]/g;
883
1072
  let match;
884
1073
  while ((match = npmRegex.exec(code)) !== null) {
@@ -910,7 +1099,7 @@ function extractImports(code) {
910
1099
  * Attempt to derive an npm-style package specifier from a URL import.
911
1100
  */
912
1101
  function extractPackageFromUrl(url) {
913
- return url.match(/\/(@?[^@\/]+(?:\/[^@\/]+)?)/)?.[1] ?? null;
1102
+ return /\/(@?[^@/]+(?:\/[^@/]+)?)/.exec(url)?.[1] ?? null;
914
1103
  }
915
1104
 
916
1105
  //#endregion
@@ -963,29 +1152,31 @@ var Installer = class {
963
1152
  },
964
1153
  progress: () => {},
965
1154
  finished: (files) => {
966
- if (files && files.size > 0) debug("ATA: type acquisition complete");
1155
+ if (files.size > 0) debug("ATA: type acquisition complete");
967
1156
  },
968
1157
  errorMessage: (message, error$1) => {
969
1158
  warn(`ATA warning: ${message}`, error$1);
970
1159
  },
971
- receivedFile: async (code, receivedPath) => {
972
- const normalized = receivedPath.replace(/^\//, "");
973
- const destination = path.join(this.projectDir, normalized);
974
- const pkgMatch = receivedPath.match(/\/node_modules\/(@?[^\/]+(?:\/[^\/]+)?)\//);
975
- try {
976
- if (await fs.readFile(destination, "utf-8") === code) {
977
- if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
978
- seenPackages.add(pkgMatch[1]);
979
- debug(`📦 Types: ${pkgMatch[1]} (from disk cache)`);
1160
+ receivedFile: (code, receivedPath) => {
1161
+ (async () => {
1162
+ const normalized = receivedPath.replace(/^\//, "");
1163
+ const destination = path.join(this.projectDir, normalized);
1164
+ const pkgMatch = /\/node_modules\/(@?[^/]+(?:\/[^/]+)?)\//.exec(receivedPath);
1165
+ try {
1166
+ if (await fs.readFile(destination, "utf-8") === code) {
1167
+ if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
1168
+ seenPackages.add(pkgMatch[1]);
1169
+ debug(`📦 Types: ${pkgMatch[1]} (from disk cache)`);
1170
+ }
1171
+ return;
980
1172
  }
981
- return;
1173
+ } catch {}
1174
+ if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
1175
+ seenPackages.add(pkgMatch[1]);
1176
+ debug(`📦 Types: ${pkgMatch[1]}`);
982
1177
  }
983
- } catch {}
984
- if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
985
- seenPackages.add(pkgMatch[1]);
986
- debug(`📦 Types: ${pkgMatch[1]}`);
987
- }
988
- await this.writeTypeFile(receivedPath, code);
1178
+ await this.writeTypeFile(receivedPath, code);
1179
+ })();
989
1180
  }
990
1181
  }
991
1182
  });
@@ -1073,11 +1264,11 @@ var Installer = class {
1073
1264
  warn(`Failed to write type file ${destination}`, err);
1074
1265
  return;
1075
1266
  }
1076
- if (normalized.match(/node_modules\/@types\/[^\/]+\/index\.d\.ts$/)) await this.ensureTypesPackageJson(normalized);
1267
+ if (/node_modules\/@types\/[^/]+\/index\.d\.ts$/.exec(normalized)) await this.ensureTypesPackageJson(normalized);
1077
1268
  if (normalized.includes("node_modules/@types/react/index.d.ts")) await this.patchReactTypes(destination);
1078
1269
  }
1079
1270
  async ensureTypesPackageJson(normalizedPath) {
1080
- const pkgMatch = normalizedPath.match(/node_modules\/(@types\/[^\/]+)\//);
1271
+ const pkgMatch = /node_modules\/(@types\/[^/]+)\//.exec(normalizedPath);
1081
1272
  if (!pkgMatch) return;
1082
1273
  const pkgName = pkgMatch[1];
1083
1274
  const pkgDir = path.join(this.projectDir, "node_modules", pkgName);
@@ -1089,16 +1280,7 @@ var Installer = class {
1089
1280
  const version$1 = npmData["dist-tags"]?.latest;
1090
1281
  if (!version$1 || !npmData.versions?.[version$1]) return;
1091
1282
  const pkg = npmData.versions[version$1];
1092
- if (pkg.exports && typeof pkg.exports === "object") {
1093
- const fixExport = (value) => {
1094
- if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
1095
- if (value && typeof value === "object") {
1096
- if ((value.import || value.require) && !value.types) value.types = (value.import || value.require).replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
1097
- }
1098
- return value;
1099
- };
1100
- for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExport(pkg.exports[key]);
1101
- }
1283
+ if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
1102
1284
  await fs.mkdir(pkgDir, { recursive: true });
1103
1285
  await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
1104
1286
  } catch {}
@@ -1108,7 +1290,7 @@ var Installer = class {
1108
1290
  let content = await fs.readFile(destination, "utf-8");
1109
1291
  if (content.includes("function useRef<T = undefined>()")) return;
1110
1292
  const overloadPattern = /function useRef<T>\(initialValue: T \| undefined\): RefObject<T \| undefined>;/;
1111
- if (!overloadPattern.test(content)) return;
1293
+ if (!content.includes("function useRef<T>(initialValue: T | undefined)")) return;
1112
1294
  content = content.replace(overloadPattern, `function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
1113
1295
  function useRef<T = undefined>(): MutableRefObject<T | undefined>;`);
1114
1296
  await fs.writeFile(destination, content, "utf-8");
@@ -1248,11 +1430,24 @@ declare module "*.json"
1248
1430
  }));
1249
1431
  }
1250
1432
  };
1433
+ /**
1434
+ * Transform package.json exports to include .d.ts type paths
1435
+ */
1436
+ function fixExportTypes(value) {
1437
+ if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
1438
+ if ((value.import ?? value.require) && !value.types) value.types = (value.import ?? value.require)?.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
1439
+ return value;
1440
+ }
1251
1441
  async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1252
- const urlString = typeof url === "string" ? url : url.toString();
1442
+ let urlString;
1443
+ if (typeof url === "string") urlString = url;
1444
+ else if (url instanceof URL) urlString = url.href;
1445
+ else urlString = url.url;
1253
1446
  for (let attempt = 1; attempt <= retries; attempt++) {
1254
1447
  const controller = new AbortController();
1255
- const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1448
+ const timeout = setTimeout(() => {
1449
+ controller.abort();
1450
+ }, FETCH_TIMEOUT_MS);
1256
1451
  try {
1257
1452
  const response = await fetch(url, {
1258
1453
  ...init,
@@ -1260,12 +1455,13 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1260
1455
  });
1261
1456
  clearTimeout(timeout);
1262
1457
  return response;
1263
- } catch (error$1) {
1458
+ } catch (err) {
1264
1459
  clearTimeout(timeout);
1265
- const isRetryable = error$1?.cause?.code === "ECONNRESET" || error$1?.cause?.code === "ETIMEDOUT" || error$1?.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error$1?.message?.includes("timeout");
1460
+ const error$1 = err;
1461
+ const isRetryable = error$1.cause?.code === "ECONNRESET" || error$1.cause?.code === "ETIMEDOUT" || error$1.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error$1.message.includes("timeout");
1266
1462
  if (attempt < retries && isRetryable) {
1267
1463
  const delay = attempt * 1e3;
1268
- warn(`Fetch failed (${error$1?.cause?.code || error$1?.message}) for ${urlString}, retrying in ${delay}ms...`);
1464
+ warn(`Fetch failed (${error$1.cause?.code ?? error$1.message}) for ${urlString}, retrying in ${delay}ms...`);
1269
1465
  await new Promise((resolve) => setTimeout(resolve, delay));
1270
1466
  continue;
1271
1467
  }
@@ -1397,11 +1593,12 @@ var FileMetadataCache = class {
1397
1593
  if (this.pendingPersist) await this.pendingPersist;
1398
1594
  }
1399
1595
  schedulePersist() {
1400
- if (!this.projectDir) return;
1401
- if (!this.pendingPersist) this.pendingPersist = (async () => {
1596
+ const projectDir = this.projectDir;
1597
+ if (!projectDir) return;
1598
+ this.pendingPersist ??= (async () => {
1402
1599
  try {
1403
1600
  await Promise.resolve();
1404
- await savePersistedState(this.projectDir, this.persisted);
1601
+ await savePersistedState(projectDir, this.persisted);
1405
1602
  } finally {
1406
1603
  this.pendingPersist = null;
1407
1604
  }
@@ -1420,12 +1617,24 @@ var PluginDisconnectedError = class extends Error {
1420
1617
  var UserActionCoordinator = class {
1421
1618
  pendingActions = /* @__PURE__ */ new Map();
1422
1619
  /**
1620
+ * Register a pending action and return a typed promise
1621
+ */
1622
+ awaitAction(actionId, description) {
1623
+ return new Promise((resolve, reject) => {
1624
+ this.pendingActions.set(actionId, {
1625
+ resolve,
1626
+ reject
1627
+ });
1628
+ debug(`Awaiting ${description}: ${actionId}`);
1629
+ });
1630
+ }
1631
+ /**
1423
1632
  * Sends the delete request to the plugin and awaits the user's decision
1424
1633
  */
1425
1634
  async requestDeleteDecision(socket, { fileName, requireConfirmation }) {
1426
1635
  if (!socket) throw new Error("Cannot request delete decision: plugin not connected");
1427
1636
  if (requireConfirmation) {
1428
- const confirmationPromise = this.awaitConfirmation(`delete:${fileName}`, "delete confirmation");
1637
+ const confirmationPromise = this.awaitAction(`delete:${fileName}`, "delete confirmation");
1429
1638
  await sendMessage(socket, {
1430
1639
  type: "file-delete",
1431
1640
  fileNames: [fileName],
@@ -1456,7 +1665,7 @@ var UserActionCoordinator = class {
1456
1665
  if (conflicts.length === 0) return /* @__PURE__ */ new Map();
1457
1666
  const pending = conflicts.map((conflict) => ({
1458
1667
  fileName: conflict.fileName,
1459
- promise: this.awaitConfirmation(`conflict:${conflict.fileName}`, "conflict resolution")
1668
+ promise: this.awaitAction(`conflict:${conflict.fileName}`, "conflict resolution")
1460
1669
  }));
1461
1670
  await sendMessage(socket, {
1462
1671
  type: "conflicts-detected",
@@ -1474,18 +1683,6 @@ var UserActionCoordinator = class {
1474
1683
  }
1475
1684
  }
1476
1685
  /**
1477
- * Generic confirmation awaiter
1478
- */
1479
- awaitConfirmation(actionId, description) {
1480
- return new Promise((resolve, reject) => {
1481
- this.pendingActions.set(actionId, {
1482
- resolve,
1483
- reject
1484
- });
1485
- debug(`Awaiting ${description}: ${actionId}`);
1486
- });
1487
- }
1488
- /**
1489
1686
  * Handle incoming confirmation response
1490
1687
  */
1491
1688
  handleConfirmation(actionId, value) {
@@ -2189,14 +2386,16 @@ async function executeEffect(effect, context) {
2189
2386
  status("Watching for changes...");
2190
2387
  return [];
2191
2388
  }
2192
- case "LOG":
2193
- ({
2389
+ case "LOG": {
2390
+ const logFn = {
2194
2391
  info,
2195
2392
  warn,
2196
2393
  success,
2197
2394
  debug
2198
- }[effect.level] ?? debug)(effect.message);
2395
+ }[effect.level];
2396
+ logFn(effect.message);
2199
2397
  return [];
2398
+ }
2200
2399
  }
2201
2400
  }
2202
2401
  /**
@@ -2236,7 +2435,7 @@ async function start(config) {
2236
2435
  }
2237
2436
  }
2238
2437
  const connection = await initConnection(config.port);
2239
- connection.on("handshake", async (client, message) => {
2438
+ connection.on("handshake", (client, message) => {
2240
2439
  debug(`Received handshake: ${message.projectName} (${message.projectId})`);
2241
2440
  const expectedShort = shortProjectHash(config.projectHash);
2242
2441
  const receivedShort = shortProjectHash(message.projectId);
@@ -2245,24 +2444,26 @@ async function start(config) {
2245
2444
  client.close();
2246
2445
  return;
2247
2446
  }
2248
- await processEvent({
2249
- type: "HANDSHAKE",
2250
- socket: client,
2251
- projectInfo: {
2252
- projectId: message.projectId,
2253
- projectName: message.projectName
2254
- }
2255
- });
2256
- if (config.projectDir && !installer) {
2257
- installer = new Installer({
2258
- projectDir: config.projectDir,
2259
- allowUnsupportedNpm: config.allowUnsupportedNpm
2447
+ (async () => {
2448
+ await processEvent({
2449
+ type: "HANDSHAKE",
2450
+ socket: client,
2451
+ projectInfo: {
2452
+ projectId: message.projectId,
2453
+ projectName: message.projectName
2454
+ }
2260
2455
  });
2261
- await installer.initialize();
2262
- startWatcher();
2263
- }
2264
- cancelDisconnectMessage();
2265
- if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
2456
+ if (config.projectDir && !installer) {
2457
+ installer = new Installer({
2458
+ projectDir: config.projectDir,
2459
+ allowUnsupportedNpm: config.allowUnsupportedNpm
2460
+ });
2461
+ await installer.initialize();
2462
+ startWatcher();
2463
+ }
2464
+ cancelDisconnectMessage();
2465
+ if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
2466
+ })();
2266
2467
  });
2267
2468
  async function handleMessage(message) {
2268
2469
  if (!config.projectDir || !installer) {
@@ -2340,21 +2541,25 @@ async function start(config) {
2340
2541
  warn(`Unhandled message type: ${message.type}`);
2341
2542
  return;
2342
2543
  }
2343
- if (event) await processEvent(event);
2544
+ await processEvent(event);
2344
2545
  }
2345
- connection.on("message", async (message) => {
2346
- try {
2347
- await handleMessage(message);
2348
- } catch (err) {
2349
- error("Error handling message:", err);
2350
- }
2546
+ connection.on("message", (message) => {
2547
+ (async () => {
2548
+ try {
2549
+ await handleMessage(message);
2550
+ } catch (err) {
2551
+ error("Error handling message:", err);
2552
+ }
2553
+ })();
2351
2554
  });
2352
- connection.on("disconnect", async () => {
2555
+ connection.on("disconnect", () => {
2353
2556
  scheduleDisconnectMessage(() => {
2354
2557
  status("Disconnected, waiting to reconnect...");
2355
2558
  });
2356
- await processEvent({ type: "DISCONNECT" });
2357
- userActions.cleanup();
2559
+ (async () => {
2560
+ await processEvent({ type: "DISCONNECT" });
2561
+ userActions.cleanup();
2562
+ })();
2358
2563
  });
2359
2564
  connection.on("error", (err) => {
2360
2565
  error("Error on WebSocket connection:", err);
@@ -2363,19 +2568,21 @@ async function start(config) {
2363
2568
  const startWatcher = () => {
2364
2569
  if (!config.filesDir || watcher) return;
2365
2570
  watcher = initWatcher(config.filesDir);
2366
- watcher.on("change", async (event) => {
2367
- await processEvent({
2571
+ watcher.on("change", (event) => {
2572
+ processEvent({
2368
2573
  type: "WATCHER_EVENT",
2369
2574
  event
2370
2575
  });
2371
2576
  });
2372
2577
  };
2373
- process.on("SIGINT", async () => {
2578
+ process.on("SIGINT", () => {
2374
2579
  console.log();
2375
2580
  status("Shutting down...");
2376
- if (watcher) await watcher.close();
2377
- connection.close();
2378
- process.exit(0);
2581
+ (async () => {
2582
+ if (watcher) await watcher.close();
2583
+ connection.close();
2584
+ process.exit(0);
2585
+ })();
2379
2586
  });
2380
2587
  }
2381
2588
 
@@ -2392,7 +2599,7 @@ const program = new Command();
2392
2599
  program.exitOverride((err) => {
2393
2600
  if (err.code === "commander.missingArgument") {
2394
2601
  console.error("Missing Project ID. Copy command via Code Link Plugin.");
2395
- process.exit(err.exitCode ?? 1);
2602
+ process.exit(err.exitCode);
2396
2603
  }
2397
2604
  throw err;
2398
2605
  });
@@ -2406,7 +2613,6 @@ program.name("framer-code-link").description("Sync Framer code components to you
2406
2613
  process.exit(1);
2407
2614
  }
2408
2615
  }
2409
- const isDev = process.env.NODE_ENV === "development";
2410
2616
  if (options.logLevel) {
2411
2617
  const level = {
2412
2618
  debug: LogLevel.DEBUG,
@@ -2415,7 +2621,7 @@ program.name("framer-code-link").description("Sync Framer code components to you
2415
2621
  error: LogLevel.ERROR
2416
2622
  }[options.logLevel.toLowerCase()];
2417
2623
  if (level !== void 0) setLogLevel(level);
2418
- } else if (options.verbose || isDev) setLogLevel(LogLevel.DEBUG);
2624
+ } else if (options.verbose) setLogLevel(LogLevel.DEBUG);
2419
2625
  const port = getPortFromHash(projectHash);
2420
2626
  banner(version, port);
2421
2627
  const config = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",
@@ -10,7 +10,7 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "dev": "NODE_ENV=development tsx src/index.ts",
13
- "build": "tsdown src/index.ts",
13
+ "build": "tsdown",
14
14
  "start": "node dist/index.mjs",
15
15
  "test": "vitest run"
16
16
  },