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.
- package/dist/index.mjs +325 -119
- 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, () =>
|
|
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, () =>
|
|
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, () =>
|
|
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, () =>
|
|
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
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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) =>
|
|
489
|
-
|
|
490
|
-
|
|
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(
|
|
493
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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+['"]([
|
|
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
|
|
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
|
|
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:
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
if (
|
|
978
|
-
seenPackages.
|
|
979
|
-
|
|
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
|
-
|
|
1173
|
+
} catch {}
|
|
1174
|
+
if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
|
|
1175
|
+
seenPackages.add(pkgMatch[1]);
|
|
1176
|
+
debug(`📦 Types: ${pkgMatch[1]}`);
|
|
982
1177
|
}
|
|
983
|
-
|
|
984
|
-
|
|
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 (
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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(() =>
|
|
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 (
|
|
1458
|
+
} catch (err) {
|
|
1264
1459
|
clearTimeout(timeout);
|
|
1265
|
-
const
|
|
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
|
|
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
|
-
|
|
1401
|
-
if (!
|
|
1596
|
+
const projectDir = this.projectDir;
|
|
1597
|
+
if (!projectDir) return;
|
|
1598
|
+
this.pendingPersist ??= (async () => {
|
|
1402
1599
|
try {
|
|
1403
1600
|
await Promise.resolve();
|
|
1404
|
-
await savePersistedState(
|
|
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.
|
|
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.
|
|
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]
|
|
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",
|
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
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
|
-
|
|
2544
|
+
await processEvent(event);
|
|
2344
2545
|
}
|
|
2345
|
-
connection.on("message",
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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",
|
|
2555
|
+
connection.on("disconnect", () => {
|
|
2353
2556
|
scheduleDisconnectMessage(() => {
|
|
2354
2557
|
status("Disconnected, waiting to reconnect...");
|
|
2355
2558
|
});
|
|
2356
|
-
|
|
2357
|
-
|
|
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",
|
|
2367
|
-
|
|
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",
|
|
2578
|
+
process.on("SIGINT", () => {
|
|
2374
2579
|
console.log();
|
|
2375
2580
|
status("Shutting down...");
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
13
|
+
"build": "tsdown",
|
|
14
14
|
"start": "node dist/index.mjs",
|
|
15
15
|
"test": "vitest run"
|
|
16
16
|
},
|