framer-code-link 0.4.5 → 0.4.7

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 +84 -68
  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("/");
@@ -156,6 +156,13 @@ function normalizePath(filePath) {
156
156
  if (isAbsolute) return `/${normalized}`;
157
157
  return normalized;
158
158
  }
159
+ function normalizeCodeFilePath(filePath) {
160
+ const normalized = normalizePath$1(filePath);
161
+ return normalized.startsWith("/") ? normalized.slice(1) : normalized;
162
+ }
163
+ function canonicalFileName(filePath) {
164
+ return normalizeCodeFilePath(filePath);
165
+ }
159
166
  function sanitizeFilePath(input, capitalizeReactComponent = true) {
160
167
  const trimmed = input.trim();
161
168
  const [inputName, extension] = splitExtension(filename(trimmed));
@@ -170,10 +177,17 @@ function sanitizeFilePath(input, capitalizeReactComponent = true) {
170
177
  extension
171
178
  };
172
179
  }
173
- function isSupportedExtension(filePath) {
180
+ function isSupportedExtension$1(filePath) {
174
181
  return /\.(tsx?|jsx?|json)$/i.test(filePath);
175
182
  }
176
183
  /**
184
+ * Returns a normalized, lowercase key for case-insensitive file lookups.
185
+ * Use this for Map keys on operating systems where "File.tsx" and "file.tsx" are the same file.
186
+ */
187
+ function fileKeyForLookup(filePath) {
188
+ return canonicalFileName(filePath).toLowerCase();
189
+ }
190
+ /**
177
191
  * Pluralize a word based on count
178
192
  * @example pluralize(1, "file") => "1 file"
179
193
  * @example pluralize(3, "file") => "3 files"
@@ -275,12 +289,12 @@ var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
275
289
  //#endregion
276
290
  //#region src/utils/logging.ts
277
291
  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;
292
+ let LogLevel = /* @__PURE__ */ function(LogLevel) {
293
+ LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
294
+ LogLevel[LogLevel["INFO"] = 1] = "INFO";
295
+ LogLevel[LogLevel["WARN"] = 2] = "WARN";
296
+ LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
297
+ return LogLevel;
284
298
  }({});
285
299
  let currentLevel = LogLevel.INFO;
286
300
  let lastMessage = "";
@@ -325,9 +339,9 @@ function logWithDedupe(message, writer) {
325
339
  /**
326
340
  * Print the startup banner - one colored line
327
341
  */
328
- function banner(version$1, port) {
342
+ function banner(version, port) {
329
343
  console.log();
330
- let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version$1}`)}`;
344
+ let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version}`)}`;
331
345
  if (currentLevel <= LogLevel.DEBUG) message += ` ${import_picocolors.default.dim("Port")} ${import_picocolors.default.yellow(port)}`;
332
346
  console.log(message);
333
347
  console.log();
@@ -597,7 +611,7 @@ function getRelativePath(projectDir, absolutePath) {
597
611
  * - Resolving . and .. segments
598
612
  * - Removing duplicate slashes
599
613
  */
600
- function normalizePath$1(filePath) {
614
+ function normalizePath(filePath) {
601
615
  if (!filePath) return "";
602
616
  const isAbsolute = filePath.startsWith("/");
603
617
  const segments = filePath.replace(/\\/g, "/").split("/");
@@ -635,7 +649,7 @@ const SUPPORTED_EXTENSIONS$1 = [
635
649
  ];
636
650
  const DEFAULT_EXTENSION$1 = ".tsx";
637
651
  function normalizePersistedFileName(fileName) {
638
- let normalized = normalizePath$1(fileName.trim());
652
+ let normalized = normalizePath(fileName.trim());
639
653
  if (!SUPPORTED_EXTENSIONS$1.some((ext) => normalized.toLowerCase().endsWith(ext))) normalized = `${normalized}${DEFAULT_EXTENSION$1}`;
640
654
  return normalized;
641
655
  }
@@ -713,10 +727,6 @@ const SUPPORTED_EXTENSIONS = [
713
727
  ];
714
728
  const DEFAULT_EXTENSION = ".tsx";
715
729
  const DEFAULT_REMOTE_DRIFT_MS = 2e3;
716
- /** Normalize file name for case-insensitive comparison (macOS/Windows compat) */
717
- function normalizeForComparison(fileName) {
718
- return fileName.toLowerCase();
719
- }
720
730
  /**
721
731
  * Lists all supported files in the files directory
722
732
  */
@@ -730,8 +740,8 @@ async function listFiles(filesDir) {
730
740
  await walk(entryPath);
731
741
  continue;
732
742
  }
733
- if (!isSupportedExtension$1(entry.name)) continue;
734
- const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
743
+ if (!isSupportedExtension(entry.name)) continue;
744
+ const sanitizedPath = sanitizeFilePath(normalizePath$1(path.relative(filesDir, entryPath)), false).path;
735
745
  try {
736
746
  const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
737
747
  files.push({
@@ -759,17 +769,17 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
759
769
  const detect = options.detectConflicts ?? true;
760
770
  const preferRemote = options.preferRemote ?? false;
761
771
  const persistedState = options.persistedState;
762
- const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
772
+ const getPersistedState = (fileName) => persistedState?.get(fileKeyForLookup(fileName));
763
773
  debug(`Detecting conflicts for ${String(remoteFiles.length)} remote files`);
764
774
  const localFiles = await listFiles(filesDir);
765
- const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
775
+ const localFileMap = new Map(localFiles.map((f) => [fileKeyForLookup(f.name), f]));
766
776
  const remoteFileMap = new Map(remoteFiles.map((f) => {
767
- return [normalizeForComparison(resolveRemoteReference(filesDir, f.name).relativePath), f];
777
+ return [fileKeyForLookup(resolveRemoteReference(filesDir, f.name).relativePath), f];
768
778
  }));
769
779
  const processedFiles = /* @__PURE__ */ new Set();
770
780
  for (const remote of remoteFiles) {
771
781
  const normalized = resolveRemoteReference(filesDir, remote.name);
772
- const normalizedKey = normalizeForComparison(normalized.relativePath);
782
+ const normalizedKey = fileKeyForLookup(normalized.relativePath);
773
783
  const local = localFileMap.get(normalizedKey);
774
784
  processedFiles.add(normalizedKey);
775
785
  const persisted = getPersistedState(normalized.relativePath);
@@ -820,7 +830,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
820
830
  });
821
831
  }
822
832
  for (const local of localFiles) {
823
- const localKey = normalizeForComparison(local.name);
833
+ const localKey = fileKeyForLookup(local.name);
824
834
  if (!processedFiles.has(localKey)) {
825
835
  const persisted = getPersistedState(local.name);
826
836
  if (persisted) {
@@ -841,8 +851,8 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
841
851
  });
842
852
  }
843
853
  }
844
- if (persistedState) for (const [fileName] of persistedState) {
845
- const normalizedKey = normalizeForComparison(fileName);
854
+ if (persistedState) for (const fileName of persistedState.keys()) {
855
+ const normalizedKey = fileKeyForLookup(fileName);
846
856
  const inLocal = localFileMap.has(normalizedKey);
847
857
  const inRemote = remoteFileMap.has(normalizedKey);
848
858
  if (!inLocal && !inRemote) debug(`[AUTO-RESOLVE] ${fileName}: deleted on both sides, no conflict`);
@@ -855,7 +865,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
855
865
  };
856
866
  }
857
867
  function autoResolveConflicts(conflicts, versions, options = {}) {
858
- const versionMap = new Map(versions.map((version$1) => [version$1.fileName, version$1.latestRemoteVersionMs]));
868
+ const versionMap = new Map(versions.map((version) => [version.fileName, version.latestRemoteVersionMs]));
859
869
  const remoteDriftMs = options.remoteDriftMs ?? DEFAULT_REMOTE_DRIFT_MS;
860
870
  const autoResolvedLocal = [];
861
871
  const autoResolvedRemote = [];
@@ -875,28 +885,31 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
875
885
  }
876
886
  continue;
877
887
  }
888
+ if (localClean) {
889
+ debug(` Local clean -> REMOTE (safe to overwrite)`);
890
+ autoResolvedRemote.push(conflict);
891
+ continue;
892
+ }
878
893
  if (!latestRemoteVersionMs) {
879
- debug(` No remote version data, keeping conflict`);
894
+ debug(` Local modified, no remote version data -> conflict`);
880
895
  remainingConflicts.push(conflict);
881
896
  continue;
882
897
  }
883
898
  if (!lastSyncedAt) {
884
- debug(` No last sync timestamp, keeping conflict`);
899
+ debug(` Local modified, no sync timestamp -> conflict`);
885
900
  remainingConflicts.push(conflict);
886
901
  continue;
887
902
  }
888
903
  debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`);
889
904
  debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`);
890
905
  const remoteUnchanged = latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs;
891
- if (remoteUnchanged && !localClean) {
906
+ const driftMargin = latestRemoteVersionMs - lastSyncedAt;
907
+ if (remoteUnchanged) {
892
908
  debug(` Remote unchanged, local changed -> LOCAL`);
909
+ if (driftMargin > 0) debug(` (within drift tolerance: ${driftMargin}ms < ${remoteDriftMs}ms threshold)`);
893
910
  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`);
911
+ } else {
912
+ debug(` Both changed -> conflict (remote ahead by ${driftMargin}ms, threshold: ${remoteDriftMs}ms)`);
900
913
  remainingConflicts.push(conflict);
901
914
  }
902
915
  }
@@ -973,15 +986,15 @@ function resolveRemoteReference(filesDir, rawName) {
973
986
  };
974
987
  }
975
988
  function sanitizeRelativePath(relativePath) {
976
- const trimmed = normalizePath(relativePath.trim());
989
+ const trimmed = normalizePath$1(relativePath.trim());
977
990
  const sanitized = sanitizeFilePath(SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext)) ? trimmed : `${trimmed}${DEFAULT_EXTENSION}`, false);
978
- const normalized = normalizePath(sanitized.path);
991
+ const normalized = normalizePath$1(sanitized.path);
979
992
  return {
980
993
  relativePath: normalized,
981
994
  extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
982
995
  };
983
996
  }
984
- function isSupportedExtension$1(fileName) {
997
+ function isSupportedExtension(fileName) {
985
998
  const lower = fileName.toLowerCase();
986
999
  return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
987
1000
  }
@@ -1082,8 +1095,8 @@ var Installer = class {
1082
1095
  finished: (files) => {
1083
1096
  if (files.size > 0) debug("ATA: type acquisition complete");
1084
1097
  },
1085
- errorMessage: (message, error$1) => {
1086
- warn(`ATA warning: ${message}`, error$1);
1098
+ errorMessage: (message, error) => {
1099
+ warn(`ATA warning: ${message}`, error);
1087
1100
  },
1088
1101
  receivedFile: (code, receivedPath) => {
1089
1102
  (async () => {
@@ -1204,9 +1217,9 @@ var Installer = class {
1204
1217
  const response = await fetch(`https://registry.npmjs.org/${pkgName}`);
1205
1218
  if (!response.ok) return;
1206
1219
  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];
1220
+ const version = npmData["dist-tags"]?.latest;
1221
+ if (!version || !npmData.versions?.[version]) return;
1222
+ const pkg = npmData.versions[version];
1210
1223
  if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
1211
1224
  await fs.mkdir(pkgDir, { recursive: true });
1212
1225
  await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
@@ -1314,11 +1327,11 @@ declare module "*.json"
1314
1327
  if (await this.hasTypePackage(reactDomDir, REACT_DOM_TYPES_VERSION, reactDomFiles)) debug("📦 React DOM types (from cache)");
1315
1328
  else await this.downloadTypePackage("@types/react-dom", REACT_DOM_TYPES_VERSION, reactDomDir, reactDomFiles);
1316
1329
  }
1317
- async hasTypePackage(destinationDir, version$1, files) {
1330
+ async hasTypePackage(destinationDir, version, files) {
1318
1331
  try {
1319
1332
  const pkgJsonPath = path.join(destinationDir, "package.json");
1320
1333
  const pkgJson = await fs.readFile(pkgJsonPath, "utf-8");
1321
- if (JSON.parse(pkgJson).version !== version$1) return false;
1334
+ if (JSON.parse(pkgJson).version !== version) return false;
1322
1335
  for (const file of files) {
1323
1336
  if (file === "package.json") continue;
1324
1337
  await fs.access(path.join(destinationDir, file));
@@ -1328,8 +1341,8 @@ declare module "*.json"
1328
1341
  return false;
1329
1342
  }
1330
1343
  }
1331
- async downloadTypePackage(pkgName, version$1, destinationDir, files) {
1332
- const baseUrl = `https://unpkg.com/${pkgName}@${version$1}`;
1344
+ async downloadTypePackage(pkgName, version, destinationDir, files) {
1345
+ const baseUrl = `https://unpkg.com/${pkgName}@${version}`;
1333
1346
  await fs.mkdir(destinationDir, { recursive: true });
1334
1347
  await Promise.all(files.map(async (file) => {
1335
1348
  const destination = path.join(destinationDir, file);
@@ -1388,17 +1401,17 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
1388
1401
  return response;
1389
1402
  } catch (err) {
1390
1403
  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");
1404
+ const error = err;
1405
+ const isRetryable = error.cause?.code === "ECONNRESET" || error.cause?.code === "ETIMEDOUT" || error.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error.message.includes("timeout");
1393
1406
  if (isRetryable) checkFatalFailure(urlString);
1394
1407
  if (attempt < retries && isRetryable) {
1395
1408
  const delay = attempt * 1e3;
1396
- warn(`Fetch failed (${error$1.cause?.code ?? error$1.message}) for ${urlString}, retrying in ${delay}ms...`);
1409
+ warn(`Fetch failed (${error.cause?.code ?? error.message}) for ${urlString}, retrying in ${delay}ms...`);
1397
1410
  await new Promise((resolve) => setTimeout(resolve, delay));
1398
1411
  continue;
1399
1412
  }
1400
- warn(`Fetch failed for ${urlString}`, error$1);
1401
- throw error$1;
1413
+ warn(`Fetch failed for ${urlString}`, error);
1414
+ throw error;
1402
1415
  }
1403
1416
  }
1404
1417
  throw new Error(`Max retries exceeded for ${urlString}`);
@@ -1561,8 +1574,8 @@ function initWatcher(filesDir) {
1561
1574
  });
1562
1575
  debug(`Watching directory: ${filesDir}`);
1563
1576
  const emitEvent = async (kind, absolutePath) => {
1564
- if (!isSupportedExtension(absolutePath)) return;
1565
- const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
1577
+ if (!isSupportedExtension$1(absolutePath)) return;
1578
+ const rawRelativePath = normalizePath$1(getRelativePath(filesDir, absolutePath));
1566
1579
  const relativePath = sanitizeFilePath(rawRelativePath, false).path;
1567
1580
  let effectiveAbsolutePath = absolutePath;
1568
1581
  if (relativePath !== rawRelativePath && kind === "add") {
@@ -1624,7 +1637,7 @@ var FileMetadataCache = class {
1624
1637
  const loaded = await loadPersistedState(projectDir);
1625
1638
  this.persisted = loaded;
1626
1639
  this.metadata = /* @__PURE__ */ new Map();
1627
- for (const [fileName, state] of loaded.entries()) this.metadata.set(fileName, {
1640
+ for (const [fileName, state] of loaded.entries()) this.metadata.set(fileKeyForLookup(fileName), {
1628
1641
  localHash: state.contentHash,
1629
1642
  lastSyncedHash: state.contentHash,
1630
1643
  lastRemoteTimestamp: state.timestamp
@@ -1632,10 +1645,10 @@ var FileMetadataCache = class {
1632
1645
  this.initialized = true;
1633
1646
  }
1634
1647
  get(fileName) {
1635
- return this.metadata.get(fileName);
1648
+ return this.metadata.get(fileKeyForLookup(fileName));
1636
1649
  }
1637
1650
  has(fileName) {
1638
- return this.metadata.has(fileName);
1651
+ return this.metadata.has(fileKeyForLookup(fileName));
1639
1652
  }
1640
1653
  size() {
1641
1654
  return this.metadata.size;
@@ -1644,33 +1657,36 @@ var FileMetadataCache = class {
1644
1657
  return this.persisted;
1645
1658
  }
1646
1659
  recordRemoteWrite(fileName, content, remoteModifiedAt) {
1660
+ const key = fileKeyForLookup(fileName);
1647
1661
  const contentHash = hashFileContent(content);
1648
- this.metadata.set(fileName, {
1662
+ this.metadata.set(key, {
1649
1663
  localHash: contentHash,
1650
1664
  lastSyncedHash: contentHash,
1651
1665
  lastRemoteTimestamp: remoteModifiedAt
1652
1666
  });
1653
- this.persisted.set(fileName, {
1667
+ this.persisted.set(key, {
1654
1668
  contentHash,
1655
1669
  timestamp: remoteModifiedAt
1656
1670
  });
1657
1671
  this.schedulePersist();
1658
1672
  }
1659
1673
  recordSyncedSnapshot(fileName, contentHash, remoteModifiedAt) {
1660
- this.metadata.set(fileName, {
1674
+ const key = fileKeyForLookup(fileName);
1675
+ this.metadata.set(key, {
1661
1676
  localHash: contentHash,
1662
1677
  lastSyncedHash: contentHash,
1663
1678
  lastRemoteTimestamp: remoteModifiedAt
1664
1679
  });
1665
- this.persisted.set(fileName, {
1680
+ this.persisted.set(key, {
1666
1681
  contentHash,
1667
1682
  timestamp: remoteModifiedAt
1668
1683
  });
1669
1684
  this.schedulePersist();
1670
1685
  }
1671
1686
  recordDelete(fileName) {
1672
- this.metadata.delete(fileName);
1673
- this.persisted.delete(fileName);
1687
+ const key = fileKeyForLookup(fileName);
1688
+ this.metadata.delete(key);
1689
+ this.persisted.delete(key);
1674
1690
  this.schedulePersist();
1675
1691
  }
1676
1692
  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.7",
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
  }