framer-code-link 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +42 -231
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ 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";
|
|
8
9
|
import { createHash } from "crypto";
|
|
9
10
|
import { setupTypeAcquisition } from "@typescript/ata";
|
|
10
11
|
import ts from "typescript";
|
|
@@ -118,7 +119,6 @@ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
|
|
|
118
119
|
let currentLevel = LogLevel.INFO;
|
|
119
120
|
let lastMessage = "";
|
|
120
121
|
let lastMessageCount = 0;
|
|
121
|
-
let lastCategory = "other";
|
|
122
122
|
const CLEAR_LINE = "\x1B[2K";
|
|
123
123
|
const MOVE_CURSOR_UP = "\x1B[1A";
|
|
124
124
|
function rewriteLastLine(text) {
|
|
@@ -144,16 +144,6 @@ function flushDedupe() {
|
|
|
144
144
|
lastMessageCount = 0;
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
* Handle category transition - adds newline when switching categories
|
|
148
|
-
*/
|
|
149
|
-
function transitionCategory(newCategory) {
|
|
150
|
-
if (lastCategory !== newCategory) {
|
|
151
|
-
flushDedupe();
|
|
152
|
-
console.log();
|
|
153
|
-
lastCategory = newCategory;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
147
|
* Log with deduplication - repeated messages within window get counted
|
|
158
148
|
*/
|
|
159
149
|
function logWithDedupe(message, writer) {
|
|
@@ -187,7 +177,6 @@ function debug(message, ...args) {
|
|
|
187
177
|
*/
|
|
188
178
|
function info(message, ...args) {
|
|
189
179
|
if (currentLevel <= LogLevel.INFO) {
|
|
190
|
-
transitionCategory("other");
|
|
191
180
|
const formatted = args.length > 0 ? `${message} ${args.join(" ")}` : message;
|
|
192
181
|
logWithDedupe(formatted, () => console.log(formatted));
|
|
193
182
|
}
|
|
@@ -198,7 +187,6 @@ function info(message, ...args) {
|
|
|
198
187
|
function warn(message, ...args) {
|
|
199
188
|
if (currentLevel <= LogLevel.WARN) {
|
|
200
189
|
if (message === lastMessage) return;
|
|
201
|
-
transitionCategory("other");
|
|
202
190
|
flushDedupe();
|
|
203
191
|
lastMessage = message;
|
|
204
192
|
lastMessageCount = 1;
|
|
@@ -210,7 +198,6 @@ function warn(message, ...args) {
|
|
|
210
198
|
*/
|
|
211
199
|
function error(message, ...args) {
|
|
212
200
|
if (currentLevel <= LogLevel.ERROR) {
|
|
213
|
-
transitionCategory("other");
|
|
214
201
|
flushDedupe();
|
|
215
202
|
console.error(import_picocolors.default.red(`✗ ${message}`), ...args);
|
|
216
203
|
}
|
|
@@ -220,7 +207,6 @@ function error(message, ...args) {
|
|
|
220
207
|
*/
|
|
221
208
|
function success(message, ...args) {
|
|
222
209
|
if (currentLevel <= LogLevel.INFO) {
|
|
223
|
-
transitionCategory("other");
|
|
224
210
|
flushDedupe();
|
|
225
211
|
console.log(import_picocolors.default.green(`✓ ${message}`), ...args);
|
|
226
212
|
}
|
|
@@ -230,21 +216,18 @@ function success(message, ...args) {
|
|
|
230
216
|
*/
|
|
231
217
|
function fileDown(fileName) {
|
|
232
218
|
if (currentLevel <= LogLevel.INFO) {
|
|
233
|
-
transitionCategory("file-sync");
|
|
234
219
|
const msg = ` ${import_picocolors.default.blue("↓")} ${fileName}`;
|
|
235
220
|
logWithDedupe(msg, () => console.log(msg));
|
|
236
221
|
}
|
|
237
222
|
}
|
|
238
223
|
function fileUp(fileName) {
|
|
239
224
|
if (currentLevel <= LogLevel.INFO) {
|
|
240
|
-
transitionCategory("file-sync");
|
|
241
225
|
const msg = ` ${import_picocolors.default.green("↑")} ${fileName}`;
|
|
242
226
|
logWithDedupe(msg, () => console.log(msg));
|
|
243
227
|
}
|
|
244
228
|
}
|
|
245
229
|
function fileDelete(fileName) {
|
|
246
230
|
if (currentLevel <= LogLevel.INFO) {
|
|
247
|
-
transitionCategory("file-sync");
|
|
248
231
|
const msg = ` ${import_picocolors.default.red("×")} ${fileName}`;
|
|
249
232
|
logWithDedupe(msg, () => console.log(msg));
|
|
250
233
|
}
|
|
@@ -254,7 +237,6 @@ function fileDelete(fileName) {
|
|
|
254
237
|
*/
|
|
255
238
|
function status(message) {
|
|
256
239
|
if (currentLevel <= LogLevel.INFO) {
|
|
257
|
-
transitionCategory("other");
|
|
258
240
|
flushDedupe();
|
|
259
241
|
console.log(import_picocolors.default.dim(` ${message}`));
|
|
260
242
|
}
|
|
@@ -307,8 +289,7 @@ function resetDisconnectState() {
|
|
|
307
289
|
/**
|
|
308
290
|
* WebSocket connection helper
|
|
309
291
|
*
|
|
310
|
-
*
|
|
311
|
-
* simple callbacks. Keeps raw socket API localized.
|
|
292
|
+
* Wrapper around ws.Server that normalizes handshake and surfaces callbacks.
|
|
312
293
|
*/
|
|
313
294
|
/**
|
|
314
295
|
* Initializes a WebSocket server and returns a connection interface
|
|
@@ -421,174 +402,7 @@ function sendMessage(socket, message) {
|
|
|
421
402
|
}
|
|
422
403
|
|
|
423
404
|
//#endregion
|
|
424
|
-
//#region
|
|
425
|
-
/**
|
|
426
|
-
* Base58 alphabet (no 0/O/I/l to avoid confusion)
|
|
427
|
-
*/
|
|
428
|
-
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
429
|
-
/**
|
|
430
|
-
* Derive a short, deterministic hash from the full Framer project hash.
|
|
431
|
-
* Uses a simple numeric hash encoded in base58 for compactness.
|
|
432
|
-
* Idempotent: if input is already the target length, returns it unchanged.
|
|
433
|
-
*/
|
|
434
|
-
function shortProjectHash(fullHash, length = 8) {
|
|
435
|
-
if (fullHash.length === length) return fullHash;
|
|
436
|
-
let h1 = 0;
|
|
437
|
-
let h2 = 0;
|
|
438
|
-
for (let i = 0; i < fullHash.length; i++) {
|
|
439
|
-
const char = fullHash.charCodeAt(i);
|
|
440
|
-
h1 = Math.imul(h1 ^ char, 2246822507);
|
|
441
|
-
h2 = Math.imul(h2 ^ char, 3266489909);
|
|
442
|
-
}
|
|
443
|
-
h1 ^= h2 >>> 16;
|
|
444
|
-
h2 ^= h1 >>> 13;
|
|
445
|
-
let result = "";
|
|
446
|
-
const combined = [Math.abs(h1), Math.abs(h2)];
|
|
447
|
-
for (const num of combined) {
|
|
448
|
-
let n = num >>> 0;
|
|
449
|
-
while (n > 0 && result.length < length) {
|
|
450
|
-
result += BASE58[n % 58];
|
|
451
|
-
n = Math.floor(n / 58);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
while (result.length < length) result += BASE58[0];
|
|
455
|
-
return result.slice(0, length);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
//#endregion
|
|
459
|
-
//#region ../shared/dist/ports.js
|
|
460
|
-
/**
|
|
461
|
-
* Generate a deterministic port number from a project hash (full or short).
|
|
462
|
-
* Port range: 3847-4096 (250 possible ports)
|
|
463
|
-
* Must match between CLI and plugin.
|
|
464
|
-
*
|
|
465
|
-
* Internally normalizes to the short id so both full and short inputs yield the same port.
|
|
466
|
-
*/
|
|
467
|
-
function getPortFromHash(projectHash) {
|
|
468
|
-
const shortId = shortProjectHash(projectHash);
|
|
469
|
-
let hash = 0;
|
|
470
|
-
for (let i = 0; i < shortId.length; i++) {
|
|
471
|
-
const char = shortId.charCodeAt(i);
|
|
472
|
-
hash = (hash << 5) - hash + char;
|
|
473
|
-
hash = hash & hash;
|
|
474
|
-
}
|
|
475
|
-
return 3847 + Math.abs(hash) % 250;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region ../shared/dist/paths.js
|
|
480
|
-
/**
|
|
481
|
-
* File path normalization utilities
|
|
482
|
-
* Framer code files include extensions in their paths (.tsx, .ts, etc.)
|
|
483
|
-
*/
|
|
484
|
-
const firstCharacterRegex = /^[a-zA-Z$_]/;
|
|
485
|
-
const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
|
|
486
|
-
const onlyDotsRegex = /^\.+$/;
|
|
487
|
-
const tsxExtension = ".tsx";
|
|
488
|
-
var NameType;
|
|
489
|
-
(function(NameType$1) {
|
|
490
|
-
NameType$1["Variable"] = "Variable";
|
|
491
|
-
NameType$1["Selector"] = "Selector";
|
|
492
|
-
NameType$1["Directory"] = "Directory";
|
|
493
|
-
})(NameType || (NameType = {}));
|
|
494
|
-
function sanitizedName(type, name) {
|
|
495
|
-
if (!name) return null;
|
|
496
|
-
let validName = name.trim();
|
|
497
|
-
if (validName.length === 0) return null;
|
|
498
|
-
const validFirstChar = type === NameType.Selector ? "_" : "$";
|
|
499
|
-
if (type === NameType.Directory) {
|
|
500
|
-
if (onlyDotsRegex.test(validName)) return null;
|
|
501
|
-
} else if (!firstCharacterRegex.test(validName)) validName = validFirstChar + validName;
|
|
502
|
-
validName = validName.replace(remainingCharactersRegex, "_");
|
|
503
|
-
validName = validName.replace(/_+/g, "_");
|
|
504
|
-
validName = validName.replace(/^\$_/u, validFirstChar);
|
|
505
|
-
return validName;
|
|
506
|
-
}
|
|
507
|
-
function sanitizedVariableName(name) {
|
|
508
|
-
return sanitizedName(NameType.Variable, name);
|
|
509
|
-
}
|
|
510
|
-
function sanitizedDirectoryName(name) {
|
|
511
|
-
return sanitizedName(NameType.Directory, name);
|
|
512
|
-
}
|
|
513
|
-
function capitalizeFirstLetter(str) {
|
|
514
|
-
if (str.length === 0) return str;
|
|
515
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
516
|
-
}
|
|
517
|
-
function hasValidExtension(fileName) {
|
|
518
|
-
if (fileName.endsWith(".json")) return true;
|
|
519
|
-
return /\.[tj]sx?$/u.test(fileName);
|
|
520
|
-
}
|
|
521
|
-
function splitExtension(fileName) {
|
|
522
|
-
const match = fileName.match(/^(.+?)(\.[^.]+)?$/);
|
|
523
|
-
if (!match) return [fileName, ""];
|
|
524
|
-
return [match[1], match[2]?.slice(1) || ""];
|
|
525
|
-
}
|
|
526
|
-
function dirname(filePath) {
|
|
527
|
-
const at = filePath.lastIndexOf("/");
|
|
528
|
-
if (at < 0) return "";
|
|
529
|
-
return filePath.slice(0, at);
|
|
530
|
-
}
|
|
531
|
-
function filename(filePath) {
|
|
532
|
-
const at = filePath.lastIndexOf("/") + 1;
|
|
533
|
-
return filePath.slice(at);
|
|
534
|
-
}
|
|
535
|
-
function pathJoin(...parts) {
|
|
536
|
-
let res = "";
|
|
537
|
-
parts.forEach((part) => {
|
|
538
|
-
while (part.startsWith("/")) part = part.slice(1);
|
|
539
|
-
while (part.endsWith("/")) part = part.slice(0, -1);
|
|
540
|
-
if (part === "") return;
|
|
541
|
-
if (res !== "") res += "/";
|
|
542
|
-
res += part;
|
|
543
|
-
});
|
|
544
|
-
return res;
|
|
545
|
-
}
|
|
546
|
-
function normalizePath(filePath) {
|
|
547
|
-
if (!filePath) return "";
|
|
548
|
-
const isAbsolute = filePath.startsWith("/");
|
|
549
|
-
const segments = filePath.replace(/\\/g, "/").split("/");
|
|
550
|
-
const stack = [];
|
|
551
|
-
for (const segment of segments) {
|
|
552
|
-
if (!segment || segment === ".") continue;
|
|
553
|
-
if (segment === "..") {
|
|
554
|
-
if (stack.length > 0) stack.pop();
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
stack.push(segment);
|
|
558
|
-
}
|
|
559
|
-
const normalized = stack.join("/");
|
|
560
|
-
if (isAbsolute) return `/${normalized}`;
|
|
561
|
-
return normalized;
|
|
562
|
-
}
|
|
563
|
-
function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
564
|
-
const trimmed = input.trim();
|
|
565
|
-
let [inputName, extension] = splitExtension(filename(trimmed));
|
|
566
|
-
if (extension) extension = `.${extension}`;
|
|
567
|
-
const dirName = dirname(trimmed).split("/").map((part) => sanitizedDirectoryName(part)).filter((part) => Boolean(part)).join("/");
|
|
568
|
-
let name = sanitizedVariableName(inputName) ?? "MyComponent";
|
|
569
|
-
if ((!hasValidExtension(extension) || extension === tsxExtension) && capitalizeReactComponent) name = capitalizeFirstLetter(name);
|
|
570
|
-
return {
|
|
571
|
-
path: pathJoin(dirName, name + extension),
|
|
572
|
-
dirName,
|
|
573
|
-
name,
|
|
574
|
-
extension
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
function isSupportedExtension$1(filePath) {
|
|
578
|
-
return /\.(tsx?|jsx?|json)$/i.test(filePath);
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Pluralize a word based on count
|
|
582
|
-
* @example pluralize(1, "file") => "1 file"
|
|
583
|
-
* @example pluralize(3, "file") => "3 files"
|
|
584
|
-
* @example pluralize(0, "conflict") => "0 conflicts"
|
|
585
|
-
*/
|
|
586
|
-
function pluralize(count, singular, plural) {
|
|
587
|
-
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
//#endregion
|
|
591
|
-
//#region src/utils/paths.ts
|
|
405
|
+
//#region src/utils/node-paths.ts
|
|
592
406
|
/**
|
|
593
407
|
* Path manipulation utilities
|
|
594
408
|
*/
|
|
@@ -627,9 +441,7 @@ function normalizePath$1(filePath) {
|
|
|
627
441
|
/**
|
|
628
442
|
* File watcher helper
|
|
629
443
|
*
|
|
630
|
-
*
|
|
631
|
-
* only supported file types (ts, tsx, js, json). Controller never worries
|
|
632
|
-
* about addDir or platform separators.
|
|
444
|
+
* Wrapper around chokidar that normalizes file paths and filters to ts, tsx, js, json.
|
|
633
445
|
*/
|
|
634
446
|
/**
|
|
635
447
|
* Initializes a file watcher for the given directory
|
|
@@ -643,7 +455,7 @@ function initWatcher(filesDir) {
|
|
|
643
455
|
});
|
|
644
456
|
debug(`Watching directory: ${filesDir}`);
|
|
645
457
|
const emitEvent = async (kind, absolutePath) => {
|
|
646
|
-
if (!isSupportedExtension
|
|
458
|
+
if (!isSupportedExtension(absolutePath)) return;
|
|
647
459
|
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
648
460
|
const relativePath = sanitizeFilePath(rawRelativePath, false).path;
|
|
649
461
|
let effectiveAbsolutePath = absolutePath;
|
|
@@ -696,7 +508,7 @@ function initWatcher(filesDir) {
|
|
|
696
508
|
* (hash matches), because that means the file wasn't edited while CLI was offline.
|
|
697
509
|
*/
|
|
698
510
|
const STATE_FILE_NAME = ".framer-sync-state.json";
|
|
699
|
-
const CURRENT_VERSION =
|
|
511
|
+
const CURRENT_VERSION = 1;
|
|
700
512
|
const SUPPORTED_EXTENSIONS$1 = [
|
|
701
513
|
".ts",
|
|
702
514
|
".tsx",
|
|
@@ -801,7 +613,7 @@ async function listFiles(filesDir) {
|
|
|
801
613
|
await walk(entryPath);
|
|
802
614
|
continue;
|
|
803
615
|
}
|
|
804
|
-
if (!isSupportedExtension(entry.name)) continue;
|
|
616
|
+
if (!isSupportedExtension$1(entry.name)) continue;
|
|
805
617
|
const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
|
|
806
618
|
try {
|
|
807
619
|
const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
|
|
@@ -1026,6 +838,16 @@ async function readFileSafe(fileName, filesDir) {
|
|
|
1026
838
|
return null;
|
|
1027
839
|
}
|
|
1028
840
|
}
|
|
841
|
+
/**
|
|
842
|
+
* Filter out files whose content matches the last remembered hash.
|
|
843
|
+
* Used to skip inbound echoes of our own local sends.
|
|
844
|
+
*/
|
|
845
|
+
function filterEchoedFiles(files, hashTracker) {
|
|
846
|
+
return files.filter((file) => {
|
|
847
|
+
if (file.content === void 0) return true;
|
|
848
|
+
return !hashTracker.shouldSkip(file.name, file.content);
|
|
849
|
+
});
|
|
850
|
+
}
|
|
1029
851
|
function resolveRemoteReference(filesDir, rawName) {
|
|
1030
852
|
const normalized = sanitizeRelativePath(rawName);
|
|
1031
853
|
const absolutePath = path.join(filesDir, normalized.relativePath);
|
|
@@ -1043,7 +865,7 @@ function sanitizeRelativePath(relativePath) {
|
|
|
1043
865
|
extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
|
|
1044
866
|
};
|
|
1045
867
|
}
|
|
1046
|
-
function isSupportedExtension(fileName) {
|
|
868
|
+
function isSupportedExtension$1(fileName) {
|
|
1047
869
|
const lower = fileName.toLowerCase();
|
|
1048
870
|
return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
1049
871
|
}
|
|
@@ -1210,10 +1032,10 @@ var Installer = class {
|
|
|
1210
1032
|
});
|
|
1211
1033
|
}
|
|
1212
1034
|
async processImports(fileName, content) {
|
|
1213
|
-
const allImports = extractImports(content).filter((
|
|
1035
|
+
const allImports = extractImports(content).filter((i) => i.type === "npm");
|
|
1214
1036
|
if (allImports.length === 0) return;
|
|
1215
|
-
const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((
|
|
1216
|
-
if (allImports.length - imports.length > 0 && !this.allowUnsupportedNpm) debug(`Skipping unsupported packages: ${allImports.filter((
|
|
1037
|
+
const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((i) => this.isSupportedPackage(i.name));
|
|
1038
|
+
if (allImports.length - imports.length > 0 && !this.allowUnsupportedNpm) debug(`Skipping unsupported packages: ${allImports.filter((i) => !this.isSupportedPackage(i.name)).map((i) => i.name).join(", ")} (use --unsupported-npm to enable)`);
|
|
1217
1039
|
if (imports.length === 0) return;
|
|
1218
1040
|
const hash = imports.map((imp) => imp.name).sort().join(",");
|
|
1219
1041
|
if (this.processedImports.has(hash)) return;
|
|
@@ -1700,7 +1522,7 @@ var UserActionCoordinator = class {
|
|
|
1700
1522
|
* Note: This is for INCOMING changes from remote. Local changes (from watcher)
|
|
1701
1523
|
* are handled separately and always sent during watching mode.
|
|
1702
1524
|
*/
|
|
1703
|
-
function validateIncomingChange(
|
|
1525
|
+
function validateIncomingChange(fileMeta, currentMode) {
|
|
1704
1526
|
if (currentMode === "snapshot_processing" || currentMode === "handshaking") return {
|
|
1705
1527
|
action: "queue",
|
|
1706
1528
|
reason: "snapshot-in-progress"
|
|
@@ -1751,7 +1573,7 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1751
1573
|
const cwd = process.cwd();
|
|
1752
1574
|
const existing = await findExistingProjectDir(cwd, projectHash);
|
|
1753
1575
|
if (existing) return existing;
|
|
1754
|
-
if (!projectName) throw new Error("
|
|
1576
|
+
if (!projectName) throw new Error("Failed to get Project name. Pass --name <project name>.");
|
|
1755
1577
|
const dirName = toDirName(projectName);
|
|
1756
1578
|
const pkgName = toPackageName(projectName);
|
|
1757
1579
|
const shortId = shortProjectHash(projectHash);
|
|
@@ -1792,9 +1614,10 @@ async function matchesProject(packageJsonPath, projectHash) {
|
|
|
1792
1614
|
//#endregion
|
|
1793
1615
|
//#region src/controller.ts
|
|
1794
1616
|
/**
|
|
1795
|
-
* Controller
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1617
|
+
* CLI Controller
|
|
1618
|
+
*
|
|
1619
|
+
* All runtime state and orchestrates the sync lifecycle.
|
|
1620
|
+
* Helpers should provide data, nevering hold control or callbacks.
|
|
1798
1621
|
*/
|
|
1799
1622
|
/** Log helper */
|
|
1800
1623
|
function log(level, message) {
|
|
@@ -1805,16 +1628,6 @@ function log(level, message) {
|
|
|
1805
1628
|
};
|
|
1806
1629
|
}
|
|
1807
1630
|
/**
|
|
1808
|
-
* Filter out files whose content matches the last remembered hash.
|
|
1809
|
-
* Used to skip inbound echoes of our own local sends.
|
|
1810
|
-
*/
|
|
1811
|
-
function filterEchoedFiles(files, hashTracker) {
|
|
1812
|
-
return files.filter((file) => {
|
|
1813
|
-
if (file.content === void 0) return true;
|
|
1814
|
-
return !hashTracker.shouldSkip(file.name, file.content);
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
/**
|
|
1818
1631
|
* Pure state transition function
|
|
1819
1632
|
* Takes current state + event, returns new state + effects to execute
|
|
1820
1633
|
*/
|
|
@@ -1844,7 +1657,7 @@ function transition(state, event) {
|
|
|
1844
1657
|
},
|
|
1845
1658
|
effects
|
|
1846
1659
|
};
|
|
1847
|
-
case "
|
|
1660
|
+
case "FILE_SYNCED_CONFIRMATION":
|
|
1848
1661
|
effects.push(log("debug", `Remote confirmed sync: ${event.fileName}`), {
|
|
1849
1662
|
type: "UPDATE_FILE_METADATA",
|
|
1850
1663
|
fileName: event.fileName,
|
|
@@ -1905,7 +1718,7 @@ function transition(state, event) {
|
|
|
1905
1718
|
state: {
|
|
1906
1719
|
...state,
|
|
1907
1720
|
mode: "snapshot_processing",
|
|
1908
|
-
|
|
1721
|
+
pendingRemoteChanges: event.files
|
|
1909
1722
|
},
|
|
1910
1723
|
effects
|
|
1911
1724
|
};
|
|
@@ -1948,7 +1761,7 @@ function transition(state, event) {
|
|
|
1948
1761
|
effects
|
|
1949
1762
|
};
|
|
1950
1763
|
}
|
|
1951
|
-
const remoteTotal = state.
|
|
1764
|
+
const remoteTotal = state.pendingRemoteChanges.length;
|
|
1952
1765
|
const totalCount = remoteTotal + localOnly.length;
|
|
1953
1766
|
const updatedCount = safeWrites.length + localOnly.length;
|
|
1954
1767
|
const unchangedCount = Math.max(0, remoteTotal - safeWrites.length);
|
|
@@ -1962,19 +1775,19 @@ function transition(state, event) {
|
|
|
1962
1775
|
state: {
|
|
1963
1776
|
...state,
|
|
1964
1777
|
mode: "watching",
|
|
1965
|
-
|
|
1778
|
+
pendingRemoteChanges: []
|
|
1966
1779
|
},
|
|
1967
1780
|
effects
|
|
1968
1781
|
};
|
|
1969
1782
|
}
|
|
1970
1783
|
case "FILE_CHANGE": {
|
|
1971
|
-
const validation = validateIncomingChange(event.
|
|
1784
|
+
const validation = validateIncomingChange(event.fileMeta, state.mode);
|
|
1972
1785
|
if (validation.action === "queue") {
|
|
1973
1786
|
effects.push(log("debug", `Queueing file change: ${event.file.name} (${validation.reason})`));
|
|
1974
1787
|
return {
|
|
1975
1788
|
state: {
|
|
1976
1789
|
...state,
|
|
1977
|
-
|
|
1790
|
+
pendingRemoteChanges: [...state.pendingRemoteChanges, event.file]
|
|
1978
1791
|
},
|
|
1979
1792
|
effects
|
|
1980
1793
|
};
|
|
@@ -2012,7 +1825,7 @@ function transition(state, event) {
|
|
|
2012
1825
|
state,
|
|
2013
1826
|
effects
|
|
2014
1827
|
};
|
|
2015
|
-
case "
|
|
1828
|
+
case "LOCAL_DELETE_APPROVED":
|
|
2016
1829
|
effects.push(log("debug", `Delete confirmed: ${event.fileName}`), {
|
|
2017
1830
|
type: "DELETE_LOCAL_FILES",
|
|
2018
1831
|
names: [event.fileName]
|
|
@@ -2021,7 +1834,7 @@ function transition(state, event) {
|
|
|
2021
1834
|
state,
|
|
2022
1835
|
effects
|
|
2023
1836
|
};
|
|
2024
|
-
case "
|
|
1837
|
+
case "LOCAL_DELETE_REJECTED":
|
|
2025
1838
|
effects.push(log("debug", `Delete cancelled: ${event.fileName}`));
|
|
2026
1839
|
effects.push({
|
|
2027
1840
|
type: "WRITE_FILES",
|
|
@@ -2194,7 +2007,7 @@ function transition(state, event) {
|
|
|
2194
2007
|
state: {
|
|
2195
2008
|
...rest,
|
|
2196
2009
|
mode: "watching",
|
|
2197
|
-
|
|
2010
|
+
pendingRemoteChanges: []
|
|
2198
2011
|
},
|
|
2199
2012
|
effects
|
|
2200
2013
|
};
|
|
@@ -2397,7 +2210,7 @@ async function start(config) {
|
|
|
2397
2210
|
let syncState = {
|
|
2398
2211
|
mode: "disconnected",
|
|
2399
2212
|
socket: null,
|
|
2400
|
-
|
|
2213
|
+
pendingRemoteChanges: [],
|
|
2401
2214
|
pendingOperations: /* @__PURE__ */ new Map(),
|
|
2402
2215
|
nextOperationId: 1
|
|
2403
2216
|
};
|
|
@@ -2461,15 +2274,13 @@ async function start(config) {
|
|
|
2461
2274
|
case "request-files":
|
|
2462
2275
|
event = { type: "REQUEST_FILES" };
|
|
2463
2276
|
break;
|
|
2464
|
-
case "file-list":
|
|
2465
|
-
|
|
2466
|
-
debug(`Received file list: ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB)`);
|
|
2277
|
+
case "file-list":
|
|
2278
|
+
debug(`Received file list: ${message.files.length} files`);
|
|
2467
2279
|
event = {
|
|
2468
2280
|
type: "FILE_LIST",
|
|
2469
2281
|
files: message.files
|
|
2470
2282
|
};
|
|
2471
2283
|
break;
|
|
2472
|
-
}
|
|
2473
2284
|
case "file-change":
|
|
2474
2285
|
event = {
|
|
2475
2286
|
type: "FILE_CHANGE",
|
|
@@ -2491,7 +2302,7 @@ async function start(config) {
|
|
|
2491
2302
|
const unmatched = [];
|
|
2492
2303
|
for (const fileName of message.fileNames) if (!userActions.handleConfirmation(`delete:${fileName}`, true)) unmatched.push(fileName);
|
|
2493
2304
|
for (const fileName of unmatched) await processEvent({
|
|
2494
|
-
type: "
|
|
2305
|
+
type: "LOCAL_DELETE_APPROVED",
|
|
2495
2306
|
fileName
|
|
2496
2307
|
});
|
|
2497
2308
|
return;
|
|
@@ -2500,7 +2311,7 @@ async function start(config) {
|
|
|
2500
2311
|
for (const file of message.files) {
|
|
2501
2312
|
userActions.handleConfirmation(`delete:${file.fileName}`, false);
|
|
2502
2313
|
await processEvent({
|
|
2503
|
-
type: "
|
|
2314
|
+
type: "LOCAL_DELETE_REJECTED",
|
|
2504
2315
|
fileName: file.fileName,
|
|
2505
2316
|
content: file.content ?? ""
|
|
2506
2317
|
});
|
|
@@ -2508,7 +2319,7 @@ async function start(config) {
|
|
|
2508
2319
|
return;
|
|
2509
2320
|
case "file-synced":
|
|
2510
2321
|
event = {
|
|
2511
|
-
type: "
|
|
2322
|
+
type: "FILE_SYNCED_CONFIRMATION",
|
|
2512
2323
|
fileName: message.fileName,
|
|
2513
2324
|
remoteModifiedAt: message.remoteModifiedAt
|
|
2514
2325
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI tool for syncing Framer code components - controller-centric architecture",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"author": "",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@code-link/shared": "1.0.0",
|
|
25
26
|
"@typescript/ata": "^0.9.8",
|
|
26
27
|
"chokidar": "^5.0.0",
|
|
27
28
|
"commander": "^14.0.2",
|