framer-code-link 0.4.5 → 0.4.6

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 +72 -59
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -81,11 +81,11 @@ const firstCharacterRegex = /^[a-zA-Z$_]/;
81
81
  const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
82
82
  const onlyDotsRegex = /^\.+$/;
83
83
  const tsxExtension = ".tsx";
84
- var NameType = /* @__PURE__ */ function(NameType$1) {
85
- NameType$1["Variable"] = "Variable";
86
- NameType$1["Selector"] = "Selector";
87
- NameType$1["Directory"] = "Directory";
88
- return NameType$1;
84
+ var NameType = /* @__PURE__ */ function(NameType) {
85
+ NameType["Variable"] = "Variable";
86
+ NameType["Selector"] = "Selector";
87
+ NameType["Directory"] = "Directory";
88
+ return NameType;
89
89
  }(NameType || {});
90
90
  function sanitizedName(type, name) {
91
91
  if (!name) return null;
@@ -139,7 +139,7 @@ function pathJoin(...parts) {
139
139
  });
140
140
  return res;
141
141
  }
142
- function normalizePath(filePath) {
142
+ function normalizePath$1(filePath) {
143
143
  if (!filePath) return "";
144
144
  const isAbsolute = filePath.startsWith("/");
145
145
  const segments = filePath.replace(/\\/g, "/").split("/");
@@ -170,7 +170,7 @@ function sanitizeFilePath(input, capitalizeReactComponent = true) {
170
170
  extension
171
171
  };
172
172
  }
173
- function isSupportedExtension(filePath) {
173
+ function isSupportedExtension$1(filePath) {
174
174
  return /\.(tsx?|jsx?|json)$/i.test(filePath);
175
175
  }
176
176
  /**
@@ -275,12 +275,12 @@ var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
275
275
  //#endregion
276
276
  //#region src/utils/logging.ts
277
277
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
278
- let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
279
- LogLevel$1[LogLevel$1["DEBUG"] = 0] = "DEBUG";
280
- LogLevel$1[LogLevel$1["INFO"] = 1] = "INFO";
281
- LogLevel$1[LogLevel$1["WARN"] = 2] = "WARN";
282
- LogLevel$1[LogLevel$1["ERROR"] = 3] = "ERROR";
283
- return LogLevel$1;
278
+ let LogLevel = /* @__PURE__ */ function(LogLevel) {
279
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
280
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
281
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
282
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
283
+ return LogLevel;
284
284
  }({});
285
285
  let currentLevel = LogLevel.INFO;
286
286
  let lastMessage = "";
@@ -325,9 +325,9 @@ function logWithDedupe(message, writer) {
325
325
  /**
326
326
  * Print the startup banner - one colored line
327
327
  */
328
- function banner(version$1, port) {
328
+ function banner(version, port) {
329
329
  console.log();
330
- let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version$1}`)}`;
330
+ let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version}`)}`;
331
331
  if (currentLevel <= LogLevel.DEBUG) message += ` ${import_picocolors.default.dim("Port")} ${import_picocolors.default.yellow(port)}`;
332
332
  console.log(message);
333
333
  console.log();
@@ -597,7 +597,7 @@ function getRelativePath(projectDir, absolutePath) {
597
597
  * - Resolving . and .. segments
598
598
  * - Removing duplicate slashes
599
599
  */
600
- function normalizePath$1(filePath) {
600
+ function normalizePath(filePath) {
601
601
  if (!filePath) return "";
602
602
  const isAbsolute = filePath.startsWith("/");
603
603
  const segments = filePath.replace(/\\/g, "/").split("/");
@@ -635,7 +635,7 @@ const SUPPORTED_EXTENSIONS$1 = [
635
635
  ];
636
636
  const DEFAULT_EXTENSION$1 = ".tsx";
637
637
  function normalizePersistedFileName(fileName) {
638
- let normalized = normalizePath$1(fileName.trim());
638
+ let normalized = normalizePath(fileName.trim());
639
639
  if (!SUPPORTED_EXTENSIONS$1.some((ext) => normalized.toLowerCase().endsWith(ext))) normalized = `${normalized}${DEFAULT_EXTENSION$1}`;
640
640
  return normalized;
641
641
  }
@@ -712,7 +712,7 @@ const SUPPORTED_EXTENSIONS = [
712
712
  ".json"
713
713
  ];
714
714
  const DEFAULT_EXTENSION = ".tsx";
715
- const DEFAULT_REMOTE_DRIFT_MS = 2e3;
715
+ const DEFAULT_REMOTE_DRIFT_MS = 2500;
716
716
  /** Normalize file name for case-insensitive comparison (macOS/Windows compat) */
717
717
  function normalizeForComparison(fileName) {
718
718
  return fileName.toLowerCase();
@@ -730,8 +730,8 @@ async function listFiles(filesDir) {
730
730
  await walk(entryPath);
731
731
  continue;
732
732
  }
733
- if (!isSupportedExtension$1(entry.name)) continue;
734
- const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
733
+ if (!isSupportedExtension(entry.name)) continue;
734
+ const sanitizedPath = sanitizeFilePath(normalizePath$1(path.relative(filesDir, entryPath)), false).path;
735
735
  try {
736
736
  const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
737
737
  files.push({
@@ -759,7 +759,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
759
759
  const detect = options.detectConflicts ?? true;
760
760
  const preferRemote = options.preferRemote ?? false;
761
761
  const persistedState = options.persistedState;
762
- const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
762
+ const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName));
763
763
  debug(`Detecting conflicts for ${String(remoteFiles.length)} remote files`);
764
764
  const localFiles = await listFiles(filesDir);
765
765
  const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
@@ -855,7 +855,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
855
855
  };
856
856
  }
857
857
  function autoResolveConflicts(conflicts, versions, options = {}) {
858
- const versionMap = new Map(versions.map((version$1) => [version$1.fileName, version$1.latestRemoteVersionMs]));
858
+ const versionMap = new Map(versions.map((version) => [version.fileName, version.latestRemoteVersionMs]));
859
859
  const remoteDriftMs = options.remoteDriftMs ?? DEFAULT_REMOTE_DRIFT_MS;
860
860
  const autoResolvedLocal = [];
861
861
  const autoResolvedRemote = [];
@@ -875,28 +875,31 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
875
875
  }
876
876
  continue;
877
877
  }
878
+ if (localClean) {
879
+ debug(` Local clean -> REMOTE (safe to overwrite)`);
880
+ autoResolvedRemote.push(conflict);
881
+ continue;
882
+ }
878
883
  if (!latestRemoteVersionMs) {
879
- debug(` No remote version data, keeping conflict`);
884
+ debug(` Local modified, no remote version data -> conflict`);
880
885
  remainingConflicts.push(conflict);
881
886
  continue;
882
887
  }
883
888
  if (!lastSyncedAt) {
884
- debug(` No last sync timestamp, keeping conflict`);
889
+ debug(` Local modified, no sync timestamp -> conflict`);
885
890
  remainingConflicts.push(conflict);
886
891
  continue;
887
892
  }
888
893
  debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`);
889
894
  debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`);
890
895
  const remoteUnchanged = latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs;
891
- if (remoteUnchanged && !localClean) {
896
+ const driftMargin = latestRemoteVersionMs - lastSyncedAt;
897
+ if (remoteUnchanged) {
892
898
  debug(` Remote unchanged, local changed -> LOCAL`);
899
+ if (driftMargin > 0) debug(` (within drift tolerance: ${driftMargin}ms < ${remoteDriftMs}ms threshold)`);
893
900
  autoResolvedLocal.push(conflict);
894
- } else if (localClean && !remoteUnchanged) {
895
- debug(` Local unchanged, remote changed -> REMOTE`);
896
- autoResolvedRemote.push(conflict);
897
- } else if (remoteUnchanged && localClean) debug(` Both unchanged, skipping`);
898
- else {
899
- debug(` Both changed, real conflict`);
901
+ } else {
902
+ debug(` Both changed -> conflict (remote ahead by ${driftMargin}ms, threshold: ${remoteDriftMs}ms)`);
900
903
  remainingConflicts.push(conflict);
901
904
  }
902
905
  }
@@ -973,15 +976,15 @@ function resolveRemoteReference(filesDir, rawName) {
973
976
  };
974
977
  }
975
978
  function sanitizeRelativePath(relativePath) {
976
- const trimmed = normalizePath(relativePath.trim());
979
+ const trimmed = normalizePath$1(relativePath.trim());
977
980
  const sanitized = sanitizeFilePath(SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext)) ? trimmed : `${trimmed}${DEFAULT_EXTENSION}`, false);
978
- const normalized = normalizePath(sanitized.path);
981
+ const normalized = normalizePath$1(sanitized.path);
979
982
  return {
980
983
  relativePath: normalized,
981
984
  extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
982
985
  };
983
986
  }
984
- function isSupportedExtension$1(fileName) {
987
+ function isSupportedExtension(fileName) {
985
988
  const lower = fileName.toLowerCase();
986
989
  return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
987
990
  }
@@ -1082,8 +1085,8 @@ var Installer = class {
1082
1085
  finished: (files) => {
1083
1086
  if (files.size > 0) debug("ATA: type acquisition complete");
1084
1087
  },
1085
- errorMessage: (message, error$1) => {
1086
- warn(`ATA warning: ${message}`, error$1);
1088
+ errorMessage: (message, error) => {
1089
+ warn(`ATA warning: ${message}`, error);
1087
1090
  },
1088
1091
  receivedFile: (code, receivedPath) => {
1089
1092
  (async () => {
@@ -1204,9 +1207,9 @@ var Installer = class {
1204
1207
  const response = await fetch(`https://registry.npmjs.org/${pkgName}`);
1205
1208
  if (!response.ok) return;
1206
1209
  const npmData = await response.json();
1207
- const version$1 = npmData["dist-tags"]?.latest;
1208
- if (!version$1 || !npmData.versions?.[version$1]) return;
1209
- const pkg = npmData.versions[version$1];
1210
+ const version = npmData["dist-tags"]?.latest;
1211
+ if (!version || !npmData.versions?.[version]) return;
1212
+ const pkg = npmData.versions[version];
1210
1213
  if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
1211
1214
  await fs.mkdir(pkgDir, { recursive: true });
1212
1215
  await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
@@ -1314,11 +1317,11 @@ declare module "*.json"
1314
1317
  if (await this.hasTypePackage(reactDomDir, REACT_DOM_TYPES_VERSION, reactDomFiles)) debug("📦 React DOM types (from cache)");
1315
1318
  else await this.downloadTypePackage("@types/react-dom", REACT_DOM_TYPES_VERSION, reactDomDir, reactDomFiles);
1316
1319
  }
1317
- async hasTypePackage(destinationDir, version$1, files) {
1320
+ async hasTypePackage(destinationDir, version, files) {
1318
1321
  try {
1319
1322
  const pkgJsonPath = path.join(destinationDir, "package.json");
1320
1323
  const pkgJson = await fs.readFile(pkgJsonPath, "utf-8");
1321
- if (JSON.parse(pkgJson).version !== version$1) return false;
1324
+ if (JSON.parse(pkgJson).version !== version) return false;
1322
1325
  for (const file of files) {
1323
1326
  if (file === "package.json") continue;
1324
1327
  await fs.access(path.join(destinationDir, file));
@@ -1328,8 +1331,8 @@ declare module "*.json"
1328
1331
  return false;
1329
1332
  }
1330
1333
  }
1331
- async downloadTypePackage(pkgName, version$1, destinationDir, files) {
1332
- const baseUrl = `https://unpkg.com/${pkgName}@${version$1}`;
1334
+ async downloadTypePackage(pkgName, version, destinationDir, files) {
1335
+ const baseUrl = `https://unpkg.com/${pkgName}@${version}`;
1333
1336
  await fs.mkdir(destinationDir, { recursive: true });
1334
1337
  await Promise.all(files.map(async (file) => {
1335
1338
  const destination = path.join(destinationDir, file);
@@ -1388,17 +1391,17 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1388
1391
  return response;
1389
1392
  } catch (err) {
1390
1393
  clearTimeout(timeout);
1391
- const error$1 = err;
1392
- 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");
1394
+ const error = err;
1395
+ const isRetryable = error.cause?.code === "ECONNRESET" || error.cause?.code === "ETIMEDOUT" || error.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error.message.includes("timeout");
1393
1396
  if (isRetryable) checkFatalFailure(urlString);
1394
1397
  if (attempt < retries && isRetryable) {
1395
1398
  const delay = attempt * 1e3;
1396
- warn(`Fetch failed (${error$1.cause?.code ?? error$1.message}) for ${urlString}, retrying in ${delay}ms...`);
1399
+ warn(`Fetch failed (${error.cause?.code ?? error.message}) for ${urlString}, retrying in ${delay}ms...`);
1397
1400
  await new Promise((resolve) => setTimeout(resolve, delay));
1398
1401
  continue;
1399
1402
  }
1400
- warn(`Fetch failed for ${urlString}`, error$1);
1401
- throw error$1;
1403
+ warn(`Fetch failed for ${urlString}`, error);
1404
+ throw error;
1402
1405
  }
1403
1406
  }
1404
1407
  throw new Error(`Max retries exceeded for ${urlString}`);
@@ -1561,8 +1564,8 @@ function initWatcher(filesDir) {
1561
1564
  });
1562
1565
  debug(`Watching directory: ${filesDir}`);
1563
1566
  const emitEvent = async (kind, absolutePath) => {
1564
- if (!isSupportedExtension(absolutePath)) return;
1565
- const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
1567
+ if (!isSupportedExtension$1(absolutePath)) return;
1568
+ const rawRelativePath = normalizePath$1(getRelativePath(filesDir, absolutePath));
1566
1569
  const relativePath = sanitizeFilePath(rawRelativePath, false).path;
1567
1570
  let effectiveAbsolutePath = absolutePath;
1568
1571
  if (relativePath !== rawRelativePath && kind === "add") {
@@ -1612,6 +1615,13 @@ function initWatcher(filesDir) {
1612
1615
 
1613
1616
  //#endregion
1614
1617
  //#region src/utils/file-metadata-cache.ts
1618
+ /**
1619
+ * In-memory cache on top of state-persistence.
1620
+ */
1621
+ /** Normalize file name for case-insensitive lookup (macOS/Windows compat) */
1622
+ function normalizeKey(fileName) {
1623
+ return fileName.toLowerCase();
1624
+ }
1615
1625
  var FileMetadataCache = class {
1616
1626
  metadata = /* @__PURE__ */ new Map();
1617
1627
  persisted = /* @__PURE__ */ new Map();
@@ -1624,7 +1634,7 @@ var FileMetadataCache = class {
1624
1634
  const loaded = await loadPersistedState(projectDir);
1625
1635
  this.persisted = loaded;
1626
1636
  this.metadata = /* @__PURE__ */ new Map();
1627
- for (const [fileName, state] of loaded.entries()) this.metadata.set(fileName, {
1637
+ for (const [fileName, state] of loaded.entries()) this.metadata.set(normalizeKey(fileName), {
1628
1638
  localHash: state.contentHash,
1629
1639
  lastSyncedHash: state.contentHash,
1630
1640
  lastRemoteTimestamp: state.timestamp
@@ -1632,10 +1642,10 @@ var FileMetadataCache = class {
1632
1642
  this.initialized = true;
1633
1643
  }
1634
1644
  get(fileName) {
1635
- return this.metadata.get(fileName);
1645
+ return this.metadata.get(normalizeKey(fileName));
1636
1646
  }
1637
1647
  has(fileName) {
1638
- return this.metadata.has(fileName);
1648
+ return this.metadata.has(normalizeKey(fileName));
1639
1649
  }
1640
1650
  size() {
1641
1651
  return this.metadata.size;
@@ -1644,33 +1654,36 @@ var FileMetadataCache = class {
1644
1654
  return this.persisted;
1645
1655
  }
1646
1656
  recordRemoteWrite(fileName, content, remoteModifiedAt) {
1657
+ const key = normalizeKey(fileName);
1647
1658
  const contentHash = hashFileContent(content);
1648
- this.metadata.set(fileName, {
1659
+ this.metadata.set(key, {
1649
1660
  localHash: contentHash,
1650
1661
  lastSyncedHash: contentHash,
1651
1662
  lastRemoteTimestamp: remoteModifiedAt
1652
1663
  });
1653
- this.persisted.set(fileName, {
1664
+ this.persisted.set(key, {
1654
1665
  contentHash,
1655
1666
  timestamp: remoteModifiedAt
1656
1667
  });
1657
1668
  this.schedulePersist();
1658
1669
  }
1659
1670
  recordSyncedSnapshot(fileName, contentHash, remoteModifiedAt) {
1660
- this.metadata.set(fileName, {
1671
+ const key = normalizeKey(fileName);
1672
+ this.metadata.set(key, {
1661
1673
  localHash: contentHash,
1662
1674
  lastSyncedHash: contentHash,
1663
1675
  lastRemoteTimestamp: remoteModifiedAt
1664
1676
  });
1665
- this.persisted.set(fileName, {
1677
+ this.persisted.set(key, {
1666
1678
  contentHash,
1667
1679
  timestamp: remoteModifiedAt
1668
1680
  });
1669
1681
  this.schedulePersist();
1670
1682
  }
1671
1683
  recordDelete(fileName) {
1672
- this.metadata.delete(fileName);
1673
- this.persisted.delete(fileName);
1684
+ const key = normalizeKey(fileName);
1685
+ this.metadata.delete(key);
1686
+ this.persisted.delete(key);
1674
1687
  this.schedulePersist();
1675
1688
  }
1676
1689
  async flush() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@typescript/ata": "^0.9.8",
26
26
  "chokidar": "^5.0.0",
27
- "commander": "^14.0.2",
27
+ "commander": "^14.0.3",
28
28
  "prettier": "^3.7.4",
29
29
  "typescript": "^5.9.3",
30
30
  "ws": "^8.18.3"
@@ -33,7 +33,7 @@
33
33
  "@code-link/shared": "1.0.0",
34
34
  "@types/node": "^22.19.2",
35
35
  "@types/ws": "^8.18.1",
36
- "tsdown": "^0.17.4",
36
+ "tsdown": "^0.20.1",
37
37
  "tsx": "^4.21.0",
38
38
  "vitest": "^4.0.15"
39
39
  }