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