framer-code-link 0.1.4 → 0.2.1
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/README.md +1 -1
- package/dist/{index.js → index.mjs} +181 -145
- package/package.json +9 -6
- package/dist/project-DhpsFg77.js +0 -53
- package/src/controller.test.ts +0 -851
- package/src/controller.ts +0 -1368
- package/src/helpers/connection.ts +0 -180
- package/src/helpers/files.test.ts +0 -117
- package/src/helpers/files.ts +0 -440
- package/src/helpers/installer.ts +0 -534
- package/src/helpers/sync-validator.ts +0 -87
- package/src/helpers/user-actions.ts +0 -158
- package/src/helpers/watcher.ts +0 -110
- package/src/index.ts +0 -111
- package/src/types.ts +0 -109
- package/src/utils/file-metadata-cache.ts +0 -121
- package/src/utils/hash-tracker.ts +0 -79
- package/src/utils/imports.ts +0 -62
- package/src/utils/logging.ts +0 -232
- package/src/utils/paths.ts +0 -76
- package/src/utils/project.ts +0 -120
- package/src/utils/state-persistence.ts +0 -138
- package/tsconfig.json +0 -14
- package/vitest.config.ts +0 -8
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "node:module";
|
|
3
2
|
import { Command } from "commander";
|
|
4
3
|
import fs from "fs/promises";
|
|
5
4
|
import { WebSocketServer } from "ws";
|
|
6
5
|
import chokidar from "chokidar";
|
|
7
6
|
import path from "path";
|
|
8
|
-
import "url";
|
|
9
7
|
import { createHash } from "crypto";
|
|
10
8
|
import { setupTypeAcquisition } from "@typescript/ata";
|
|
11
9
|
import ts from "typescript";
|
|
@@ -17,16 +15,18 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
17
15
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
18
16
|
var __getProtoOf = Object.getPrototypeOf;
|
|
19
17
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
20
|
-
var
|
|
21
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
22
|
-
};
|
|
18
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
23
19
|
var __copyProps = (to, from, except, desc) => {
|
|
24
|
-
if (from && typeof from === "object" || typeof from === "function")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
22
|
+
key = keys[i];
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: ((k) => from[k]).bind(null, key),
|
|
26
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
30
|
}
|
|
31
31
|
return to;
|
|
32
32
|
};
|
|
@@ -37,7 +37,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region ../../node_modules/picocolors/picocolors.js
|
|
40
|
-
var require_picocolors =
|
|
40
|
+
var require_picocolors = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
41
41
|
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
42
42
|
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
43
43
|
let formatter = (open, close, replace = open) => (input) => {
|
|
@@ -102,11 +102,11 @@ var require_picocolors = __commonJS({ "../../node_modules/picocolors/picocolors.
|
|
|
102
102
|
};
|
|
103
103
|
module.exports = createColors();
|
|
104
104
|
module.exports.createColors = createColors;
|
|
105
|
-
}
|
|
105
|
+
}));
|
|
106
106
|
|
|
107
107
|
//#endregion
|
|
108
108
|
//#region src/utils/logging.ts
|
|
109
|
-
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
109
|
+
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
110
110
|
let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
|
|
111
111
|
LogLevel$1[LogLevel$1["DEBUG"] = 0] = "DEBUG";
|
|
112
112
|
LogLevel$1[LogLevel$1["INFO"] = 1] = "INFO";
|
|
@@ -126,7 +126,7 @@ function rewriteLastLine(text) {
|
|
|
126
126
|
let disconnectTimer = null;
|
|
127
127
|
let isShowingDisconnect = false;
|
|
128
128
|
let hadRecentDisconnect = false;
|
|
129
|
-
const DISCONNECT_DELAY_MS =
|
|
129
|
+
const DISCONNECT_DELAY_MS = 4e3;
|
|
130
130
|
function setLogLevel(level) {
|
|
131
131
|
currentLevel = level;
|
|
132
132
|
}
|
|
@@ -184,7 +184,10 @@ function info(message, ...args) {
|
|
|
184
184
|
*/
|
|
185
185
|
function warn(message, ...args) {
|
|
186
186
|
if (currentLevel <= LogLevel.WARN) {
|
|
187
|
+
if (message === lastMessage) return;
|
|
187
188
|
flushDedupe();
|
|
189
|
+
lastMessage = message;
|
|
190
|
+
lastMessageCount = 1;
|
|
188
191
|
console.warn(import_picocolors.default.yellow(`⚠ ${message}`), ...args);
|
|
189
192
|
}
|
|
190
193
|
}
|
|
@@ -282,6 +285,12 @@ function resetDisconnectState() {
|
|
|
282
285
|
//#endregion
|
|
283
286
|
//#region src/helpers/connection.ts
|
|
284
287
|
/**
|
|
288
|
+
* WebSocket connection helper
|
|
289
|
+
*
|
|
290
|
+
* Thin wrapper around ws.Server that normalizes handshake and surfaces
|
|
291
|
+
* simple callbacks. Keeps raw socket API localized.
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
285
294
|
* Initializes a WebSocket server and returns a connection interface
|
|
286
295
|
* Returns a Promise that resolves when the server is ready, or rejects on startup errors
|
|
287
296
|
*/
|
|
@@ -301,7 +310,7 @@ function initConnection(port) {
|
|
|
301
310
|
error(` 1. Close any other terminal running Code Link for this project`);
|
|
302
311
|
error(` 2. Or run: lsof -i :${port} | grep LISTEN`);
|
|
303
312
|
error(` Then kill the process: kill -9 <PID>`);
|
|
304
|
-
reject(new Error(`Port ${port} is already in use`));
|
|
313
|
+
reject(/* @__PURE__ */ new Error(`Port ${port} is already in use`));
|
|
305
314
|
} else {
|
|
306
315
|
error(`Failed to start WebSocket server: ${err.message}`);
|
|
307
316
|
reject(err);
|
|
@@ -315,14 +324,17 @@ function initConnection(port) {
|
|
|
315
324
|
debug(`WebSocket server listening on port ${port}`);
|
|
316
325
|
wss.on("connection", (ws) => {
|
|
317
326
|
const connId = ++connectionId;
|
|
327
|
+
let handshakeReceived = false;
|
|
318
328
|
debug(`Client connected (conn ${connId})`);
|
|
319
329
|
ws.on("message", (data) => {
|
|
320
330
|
try {
|
|
321
331
|
const message = JSON.parse(data.toString());
|
|
322
332
|
if (message.type === "handshake") {
|
|
323
333
|
debug(`Received handshake (conn ${connId})`);
|
|
334
|
+
handshakeReceived = true;
|
|
324
335
|
handlers.onHandshake?.(ws, message);
|
|
325
|
-
} else handlers.onMessage?.(message);
|
|
336
|
+
} else if (handshakeReceived) handlers.onMessage?.(message);
|
|
337
|
+
else debug(`Ignoring ${message.type} before handshake (conn ${connId})`);
|
|
326
338
|
} catch (err) {
|
|
327
339
|
error(`Failed to parse message:`, err);
|
|
328
340
|
}
|
|
@@ -440,8 +452,7 @@ function getPortFromHash(projectHash) {
|
|
|
440
452
|
hash = (hash << 5) - hash + char;
|
|
441
453
|
hash = hash & hash;
|
|
442
454
|
}
|
|
443
|
-
|
|
444
|
-
return 3847 + portOffset;
|
|
455
|
+
return 3847 + Math.abs(hash) % 250;
|
|
445
456
|
}
|
|
446
457
|
|
|
447
458
|
//#endregion
|
|
@@ -536,9 +547,8 @@ function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
|
536
547
|
const dirName = dirname(trimmed).split("/").map((part) => sanitizedDirectoryName(part)).filter((part) => Boolean(part)).join("/");
|
|
537
548
|
let name = sanitizedVariableName(inputName) ?? "MyComponent";
|
|
538
549
|
if ((!hasValidExtension(extension) || extension === tsxExtension) && capitalizeReactComponent) name = capitalizeFirstLetter(name);
|
|
539
|
-
const sanitizedPath = pathJoin(dirName, name + extension);
|
|
540
550
|
return {
|
|
541
|
-
path:
|
|
551
|
+
path: pathJoin(dirName, name + extension),
|
|
542
552
|
dirName,
|
|
543
553
|
name,
|
|
544
554
|
extension
|
|
@@ -554,13 +564,15 @@ function isSupportedExtension$1(filePath) {
|
|
|
554
564
|
* @example pluralize(0, "conflict") => "0 conflicts"
|
|
555
565
|
*/
|
|
556
566
|
function pluralize(count, singular, plural) {
|
|
557
|
-
|
|
558
|
-
return `${count} ${word}`;
|
|
567
|
+
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
|
|
559
568
|
}
|
|
560
569
|
|
|
561
570
|
//#endregion
|
|
562
571
|
//#region src/utils/paths.ts
|
|
563
572
|
/**
|
|
573
|
+
* Path manipulation utilities
|
|
574
|
+
*/
|
|
575
|
+
/**
|
|
564
576
|
* Gets a relative path from the project directory
|
|
565
577
|
*/
|
|
566
578
|
function getRelativePath(projectDir, absolutePath) {
|
|
@@ -593,6 +605,13 @@ function normalizePath$1(filePath) {
|
|
|
593
605
|
//#endregion
|
|
594
606
|
//#region src/helpers/watcher.ts
|
|
595
607
|
/**
|
|
608
|
+
* File watcher helper
|
|
609
|
+
*
|
|
610
|
+
* Thin wrapper around chokidar that normalizes file paths and emits
|
|
611
|
+
* only supported file types (ts, tsx, js, json). Controller never worries
|
|
612
|
+
* about addDir or platform separators.
|
|
613
|
+
*/
|
|
614
|
+
/**
|
|
596
615
|
* Initializes a file watcher for the given directory
|
|
597
616
|
*/
|
|
598
617
|
function initWatcher(filesDir) {
|
|
@@ -606,8 +625,7 @@ function initWatcher(filesDir) {
|
|
|
606
625
|
const emitEvent = async (kind, absolutePath) => {
|
|
607
626
|
if (!isSupportedExtension$1(absolutePath)) return;
|
|
608
627
|
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
609
|
-
const
|
|
610
|
-
const relativePath = sanitized.path;
|
|
628
|
+
const relativePath = sanitizeFilePath(rawRelativePath, false).path;
|
|
611
629
|
let effectiveAbsolutePath = absolutePath;
|
|
612
630
|
if (relativePath !== rawRelativePath && kind === "add") {
|
|
613
631
|
const newAbsolutePath = path.join(filesDir, relativePath);
|
|
@@ -650,6 +668,13 @@ function initWatcher(filesDir) {
|
|
|
650
668
|
|
|
651
669
|
//#endregion
|
|
652
670
|
//#region src/utils/state-persistence.ts
|
|
671
|
+
/**
|
|
672
|
+
* State persistence helper
|
|
673
|
+
*
|
|
674
|
+
* Persists last sync timestamps along with content hashes.
|
|
675
|
+
* We only trust persisted timestamps if the file content hasn't changed
|
|
676
|
+
* (hash matches), because that means the file wasn't edited while CLI was offline.
|
|
677
|
+
*/
|
|
653
678
|
const STATE_FILE_NAME = ".framer-sync-state.json";
|
|
654
679
|
const CURRENT_VERSION = 2;
|
|
655
680
|
const SUPPORTED_EXTENSIONS$1 = [
|
|
@@ -719,6 +744,17 @@ async function savePersistedState(projectDir, state) {
|
|
|
719
744
|
|
|
720
745
|
//#endregion
|
|
721
746
|
//#region src/helpers/files.ts
|
|
747
|
+
/**
|
|
748
|
+
* File operations helper
|
|
749
|
+
*
|
|
750
|
+
* Single place that understands disk + conflicts. Provides:
|
|
751
|
+
* - listFiles: returns current filesystem state
|
|
752
|
+
* - detectConflicts: compares remote vs local and returns conflicts + safe writes
|
|
753
|
+
* - writeRemoteFiles: applies writes/deletes from remote
|
|
754
|
+
* - deleteLocalFile: removes a file from disk
|
|
755
|
+
*
|
|
756
|
+
* Controller decides WHEN to call these, but never computes conflicts itself.
|
|
757
|
+
*/
|
|
722
758
|
const SUPPORTED_EXTENSIONS = [
|
|
723
759
|
".ts",
|
|
724
760
|
".tsx",
|
|
@@ -746,9 +782,7 @@ async function listFiles(filesDir) {
|
|
|
746
782
|
continue;
|
|
747
783
|
}
|
|
748
784
|
if (!isSupportedExtension(entry.name)) continue;
|
|
749
|
-
const
|
|
750
|
-
const normalizedPath = normalizePath(relativePath);
|
|
751
|
-
const sanitizedPath = sanitizeFilePath(normalizedPath, false).path;
|
|
785
|
+
const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
|
|
752
786
|
try {
|
|
753
787
|
const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
|
|
754
788
|
files.push({
|
|
@@ -772,6 +806,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
772
806
|
const conflicts = [];
|
|
773
807
|
const writes = [];
|
|
774
808
|
const localOnly = [];
|
|
809
|
+
const unchanged = [];
|
|
775
810
|
const detect = options.detectConflicts ?? true;
|
|
776
811
|
const preferRemote = options.preferRemote ?? false;
|
|
777
812
|
const persistedState = options.persistedState;
|
|
@@ -780,8 +815,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
780
815
|
const localFiles = await listFiles(filesDir);
|
|
781
816
|
const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
|
|
782
817
|
const remoteFileMap = new Map(remoteFiles.map((f) => {
|
|
783
|
-
|
|
784
|
-
return [normalizeForComparison(normalized.relativePath), f];
|
|
818
|
+
return [normalizeForComparison(resolveRemoteReference(filesDir, f.name).relativePath), f];
|
|
785
819
|
}));
|
|
786
820
|
const processedFiles = /* @__PURE__ */ new Set();
|
|
787
821
|
for (const remote of remoteFiles) {
|
|
@@ -809,7 +843,14 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
809
843
|
});
|
|
810
844
|
continue;
|
|
811
845
|
}
|
|
812
|
-
if (local.content === remote.content)
|
|
846
|
+
if (local.content === remote.content) {
|
|
847
|
+
unchanged.push({
|
|
848
|
+
name: normalized.relativePath,
|
|
849
|
+
content: remote.content,
|
|
850
|
+
modifiedAt: remote.modifiedAt
|
|
851
|
+
});
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
813
854
|
if (!detect || preferRemote) {
|
|
814
855
|
writes.push({
|
|
815
856
|
name: normalized.relativePath,
|
|
@@ -834,13 +875,15 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
834
875
|
if (!processedFiles.has(localKey)) {
|
|
835
876
|
const persisted = getPersistedState(local.name);
|
|
836
877
|
if (persisted) {
|
|
837
|
-
|
|
878
|
+
const localClean = hashFileContent(local.content) === persisted.contentHash;
|
|
879
|
+
debug(`Conflict: ${local.name} deleted in Framer (localClean=${localClean})`);
|
|
838
880
|
conflicts.push({
|
|
839
881
|
fileName: local.name,
|
|
840
882
|
localContent: local.content,
|
|
841
883
|
remoteContent: null,
|
|
842
884
|
localModifiedAt: local.modifiedAt,
|
|
843
|
-
lastSyncedAt: persisted?.timestamp
|
|
885
|
+
lastSyncedAt: persisted?.timestamp,
|
|
886
|
+
localClean
|
|
844
887
|
});
|
|
845
888
|
} else localOnly.push({
|
|
846
889
|
name: local.name,
|
|
@@ -858,7 +901,8 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
858
901
|
return {
|
|
859
902
|
conflicts,
|
|
860
903
|
writes,
|
|
861
|
-
localOnly
|
|
904
|
+
localOnly,
|
|
905
|
+
unchanged
|
|
862
906
|
};
|
|
863
907
|
}
|
|
864
908
|
function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
@@ -870,7 +914,18 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
870
914
|
for (const conflict of conflicts) {
|
|
871
915
|
const latestRemoteVersionMs = versionMap.get(conflict.fileName);
|
|
872
916
|
const lastSyncedAt = conflict.lastSyncedAt;
|
|
917
|
+
const localClean = conflict.localClean === true;
|
|
873
918
|
debug(`Auto-resolve checking ${conflict.fileName}`);
|
|
919
|
+
if (conflict.remoteContent === null) {
|
|
920
|
+
if (localClean) {
|
|
921
|
+
debug(` Remote deleted, local clean -> REMOTE (delete locally)`);
|
|
922
|
+
autoResolvedRemote.push(conflict);
|
|
923
|
+
} else {
|
|
924
|
+
debug(` Remote deleted, local modified -> conflict`);
|
|
925
|
+
remainingConflicts.push(conflict);
|
|
926
|
+
}
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
874
929
|
if (!latestRemoteVersionMs) {
|
|
875
930
|
debug(` No remote version data, keeping conflict`);
|
|
876
931
|
remainingConflicts.push(conflict);
|
|
@@ -884,7 +939,6 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
884
939
|
debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`);
|
|
885
940
|
debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`);
|
|
886
941
|
const remoteUnchanged = latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs;
|
|
887
|
-
const localClean = conflict.localClean === true;
|
|
888
942
|
if (remoteUnchanged && !localClean) {
|
|
889
943
|
debug(` Remote unchanged, local changed -> LOCAL`);
|
|
890
944
|
autoResolvedLocal.push(conflict);
|
|
@@ -932,8 +986,7 @@ async function deleteLocalFile(fileName, filesDir, hashTracker) {
|
|
|
932
986
|
hashTracker.forget(normalized.relativePath);
|
|
933
987
|
debug(`Deleted file: ${normalized.relativePath}`);
|
|
934
988
|
} catch (err) {
|
|
935
|
-
|
|
936
|
-
if (nodeError?.code === "ENOENT") {
|
|
989
|
+
if (err?.code === "ENOENT") {
|
|
937
990
|
hashTracker.forget(normalized.relativePath);
|
|
938
991
|
debug(`File already deleted: ${normalized.relativePath}`);
|
|
939
992
|
return;
|
|
@@ -963,9 +1016,7 @@ function resolveRemoteReference(filesDir, rawName) {
|
|
|
963
1016
|
}
|
|
964
1017
|
function sanitizeRelativePath(relativePath) {
|
|
965
1018
|
const trimmed = normalizePath(relativePath.trim());
|
|
966
|
-
const
|
|
967
|
-
const candidate = hasExtension ? trimmed : `${trimmed}${DEFAULT_EXTENSION}`;
|
|
968
|
-
const sanitized = sanitizeFilePath(candidate, false);
|
|
1019
|
+
const sanitized = sanitizeFilePath(SUPPORTED_EXTENSIONS.some((ext) => trimmed.toLowerCase().endsWith(ext)) ? trimmed : `${trimmed}${DEFAULT_EXTENSION}`, false);
|
|
969
1020
|
const normalized = normalizePath(sanitized.path);
|
|
970
1021
|
return {
|
|
971
1022
|
relativePath: normalized,
|
|
@@ -1017,12 +1068,14 @@ function extractImports(code) {
|
|
|
1017
1068
|
* Attempt to derive an npm-style package specifier from a URL import.
|
|
1018
1069
|
*/
|
|
1019
1070
|
function extractPackageFromUrl(url) {
|
|
1020
|
-
|
|
1021
|
-
return match?.[1] ?? null;
|
|
1071
|
+
return url.match(/\/(@?[^@\/]+(?:\/[^@\/]+)?)/)?.[1] ?? null;
|
|
1022
1072
|
}
|
|
1023
1073
|
|
|
1024
1074
|
//#endregion
|
|
1025
1075
|
//#region src/helpers/installer.ts
|
|
1076
|
+
/**
|
|
1077
|
+
* Type installer helper using @typescript/ata
|
|
1078
|
+
*/
|
|
1026
1079
|
const FETCH_TIMEOUT_MS = 6e4;
|
|
1027
1080
|
const MAX_FETCH_RETRIES = 3;
|
|
1028
1081
|
const REACT_TYPES_VERSION = "18.3.12";
|
|
@@ -1061,11 +1114,8 @@ var Installer = class {
|
|
|
1061
1114
|
const normalized = receivedPath.replace(/^\//, "");
|
|
1062
1115
|
const destination = path.join(this.projectDir, normalized);
|
|
1063
1116
|
const pkgMatch = receivedPath.match(/\/node_modules\/(@?[^\/]+(?:\/[^\/]+)?)\//);
|
|
1064
|
-
let isFromCache = false;
|
|
1065
1117
|
try {
|
|
1066
|
-
|
|
1067
|
-
if (existing === code) {
|
|
1068
|
-
isFromCache = true;
|
|
1118
|
+
if (await fs.readFile(destination, "utf-8") === code) {
|
|
1069
1119
|
if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
|
|
1070
1120
|
seenPackages.add(pkgMatch[1]);
|
|
1071
1121
|
debug(`📦 Types: ${pkgMatch[1]} (from disk cache)`);
|
|
@@ -1164,15 +1214,9 @@ var Installer = class {
|
|
|
1164
1214
|
const pkg = npmData.versions[version];
|
|
1165
1215
|
if (pkg.exports && typeof pkg.exports === "object") {
|
|
1166
1216
|
const fixExport = (value) => {
|
|
1167
|
-
if (typeof value === "string") {
|
|
1168
|
-
const tsPath = value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
|
|
1169
|
-
return { types: tsPath };
|
|
1170
|
-
}
|
|
1217
|
+
if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
|
|
1171
1218
|
if (value && typeof value === "object") {
|
|
1172
|
-
if ((value.import || value.require) && !value.types)
|
|
1173
|
-
const base = value.import || value.require;
|
|
1174
|
-
value.types = base.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
|
|
1175
|
-
}
|
|
1219
|
+
if ((value.import || value.require) && !value.types) value.types = (value.import || value.require).replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
|
|
1176
1220
|
}
|
|
1177
1221
|
return value;
|
|
1178
1222
|
};
|
|
@@ -1199,7 +1243,7 @@ var Installer = class {
|
|
|
1199
1243
|
await fs.access(tsconfigPath);
|
|
1200
1244
|
debug("tsconfig.json already exists");
|
|
1201
1245
|
} catch {
|
|
1202
|
-
|
|
1246
|
+
await fs.writeFile(tsconfigPath, JSON.stringify({
|
|
1203
1247
|
compilerOptions: {
|
|
1204
1248
|
noEmit: true,
|
|
1205
1249
|
target: "ES2021",
|
|
@@ -1222,8 +1266,7 @@ var Installer = class {
|
|
|
1222
1266
|
typeRoots: ["./node_modules/@types"]
|
|
1223
1267
|
},
|
|
1224
1268
|
include: ["files/**/*", "framer-modules.d.ts"]
|
|
1225
|
-
};
|
|
1226
|
-
await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2));
|
|
1269
|
+
}, null, 2));
|
|
1227
1270
|
debug("Created tsconfig.json");
|
|
1228
1271
|
}
|
|
1229
1272
|
}
|
|
@@ -1233,12 +1276,11 @@ var Installer = class {
|
|
|
1233
1276
|
await fs.access(prettierPath);
|
|
1234
1277
|
debug(".prettierrc already exists");
|
|
1235
1278
|
} catch {
|
|
1236
|
-
|
|
1279
|
+
await fs.writeFile(prettierPath, JSON.stringify({
|
|
1237
1280
|
tabWidth: 4,
|
|
1238
1281
|
semi: false,
|
|
1239
1282
|
trailingComma: "es5"
|
|
1240
|
-
};
|
|
1241
|
-
await fs.writeFile(prettierPath, JSON.stringify(config, null, 2));
|
|
1283
|
+
}, null, 2));
|
|
1242
1284
|
debug("Created .prettierrc");
|
|
1243
1285
|
}
|
|
1244
1286
|
}
|
|
@@ -1248,14 +1290,13 @@ var Installer = class {
|
|
|
1248
1290
|
await fs.access(declarationsPath);
|
|
1249
1291
|
debug("framer-modules.d.ts already exists");
|
|
1250
1292
|
} catch {
|
|
1251
|
-
|
|
1293
|
+
await fs.writeFile(declarationsPath, `// Type declarations for Framer URL imports
|
|
1252
1294
|
declare module "https://framer.com/m/*"
|
|
1253
1295
|
|
|
1254
1296
|
declare module "https://framerusercontent.com/*"
|
|
1255
1297
|
|
|
1256
1298
|
declare module "*.json"
|
|
1257
|
-
|
|
1258
|
-
await fs.writeFile(declarationsPath, declarations);
|
|
1299
|
+
`);
|
|
1259
1300
|
debug("Created framer-modules.d.ts");
|
|
1260
1301
|
}
|
|
1261
1302
|
}
|
|
@@ -1302,8 +1343,7 @@ declare module "*.json"
|
|
|
1302
1343
|
try {
|
|
1303
1344
|
const pkgJsonPath = path.join(destinationDir, "package.json");
|
|
1304
1345
|
const pkgJson = await fs.readFile(pkgJsonPath, "utf-8");
|
|
1305
|
-
|
|
1306
|
-
if (parsed.version !== version) return false;
|
|
1346
|
+
if (JSON.parse(pkgJson).version !== version) return false;
|
|
1307
1347
|
for (const file of files) {
|
|
1308
1348
|
if (file === "package.json") continue;
|
|
1309
1349
|
await fs.access(path.join(destinationDir, file));
|
|
@@ -1362,6 +1402,12 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
|
|
|
1362
1402
|
//#endregion
|
|
1363
1403
|
//#region src/utils/hash-tracker.ts
|
|
1364
1404
|
/**
|
|
1405
|
+
* Hash tracking utilities for echo prevention
|
|
1406
|
+
*
|
|
1407
|
+
* The hash tracker prevents echo loops by remembering content hashes
|
|
1408
|
+
* and skipping watcher events for files we just wrote.
|
|
1409
|
+
*/
|
|
1410
|
+
/**
|
|
1365
1411
|
* Creates a hash tracker instance for echo prevention
|
|
1366
1412
|
*/
|
|
1367
1413
|
function createHashTracker() {
|
|
@@ -1374,8 +1420,7 @@ function createHashTracker() {
|
|
|
1374
1420
|
},
|
|
1375
1421
|
shouldSkip(filePath, content) {
|
|
1376
1422
|
const currentHash = hashContent(content);
|
|
1377
|
-
|
|
1378
|
-
return storedHash === currentHash;
|
|
1423
|
+
return hashes.get(filePath) === currentHash;
|
|
1379
1424
|
},
|
|
1380
1425
|
forget(filePath) {
|
|
1381
1426
|
hashes.delete(filePath);
|
|
@@ -1637,8 +1682,7 @@ async function getProjectHashFromCwd() {
|
|
|
1637
1682
|
try {
|
|
1638
1683
|
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
1639
1684
|
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
1640
|
-
|
|
1641
|
-
return pkg.shortProjectHash ?? null;
|
|
1685
|
+
return JSON.parse(content).shortProjectHash ?? null;
|
|
1642
1686
|
} catch {
|
|
1643
1687
|
return null;
|
|
1644
1688
|
}
|
|
@@ -1670,8 +1714,7 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1670
1714
|
return projectDir;
|
|
1671
1715
|
}
|
|
1672
1716
|
async function findExistingProjectDir(baseDir, projectHash) {
|
|
1673
|
-
|
|
1674
|
-
if (await matchesProject(candidate, projectHash)) return baseDir;
|
|
1717
|
+
if (await matchesProject(path.join(baseDir, "package.json"), projectHash)) return baseDir;
|
|
1675
1718
|
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
1676
1719
|
for (const entry of entries) {
|
|
1677
1720
|
if (!entry.isDirectory()) continue;
|
|
@@ -1693,6 +1736,11 @@ async function matchesProject(packageJsonPath, projectHash) {
|
|
|
1693
1736
|
|
|
1694
1737
|
//#endregion
|
|
1695
1738
|
//#region src/controller.ts
|
|
1739
|
+
/**
|
|
1740
|
+
* Controller
|
|
1741
|
+
* Single source of truth for all runtime state and orchestrates the sync lifecycle.
|
|
1742
|
+
* Helpers are functions that provide data - they never hold control or callbacks.
|
|
1743
|
+
*/
|
|
1696
1744
|
/** Log helper */
|
|
1697
1745
|
function log(level, message) {
|
|
1698
1746
|
return {
|
|
@@ -1702,13 +1750,23 @@ function log(level, message) {
|
|
|
1702
1750
|
};
|
|
1703
1751
|
}
|
|
1704
1752
|
/**
|
|
1753
|
+
* Filter out files whose content matches the last remembered hash.
|
|
1754
|
+
* Used to skip inbound echoes of our own local sends.
|
|
1755
|
+
*/
|
|
1756
|
+
function filterEchoedFiles(files, hashTracker) {
|
|
1757
|
+
return files.filter((file) => {
|
|
1758
|
+
if (file.content === void 0) return true;
|
|
1759
|
+
return !hashTracker.shouldSkip(file.name, file.content);
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1705
1763
|
* Pure state transition function
|
|
1706
1764
|
* Takes current state + event, returns new state + effects to execute
|
|
1707
1765
|
*/
|
|
1708
1766
|
function transition(state, event) {
|
|
1709
1767
|
const effects = [];
|
|
1710
1768
|
switch (event.type) {
|
|
1711
|
-
case "HANDSHAKE":
|
|
1769
|
+
case "HANDSHAKE":
|
|
1712
1770
|
if (state.mode !== "disconnected") {
|
|
1713
1771
|
effects.push(log("warn", `Received HANDSHAKE in mode ${state.mode}, ignoring`));
|
|
1714
1772
|
return {
|
|
@@ -1731,8 +1789,7 @@ function transition(state, event) {
|
|
|
1731
1789
|
},
|
|
1732
1790
|
effects
|
|
1733
1791
|
};
|
|
1734
|
-
|
|
1735
|
-
case "FILE_SYNCED": {
|
|
1792
|
+
case "FILE_SYNCED":
|
|
1736
1793
|
effects.push(log("debug", `Remote confirmed sync: ${event.fileName}`), {
|
|
1737
1794
|
type: "UPDATE_FILE_METADATA",
|
|
1738
1795
|
fileName: event.fileName,
|
|
@@ -1742,11 +1799,10 @@ function transition(state, event) {
|
|
|
1742
1799
|
state,
|
|
1743
1800
|
effects
|
|
1744
1801
|
};
|
|
1745
|
-
|
|
1746
|
-
case "DISCONNECT": {
|
|
1802
|
+
case "DISCONNECT":
|
|
1747
1803
|
effects.push({ type: "PERSIST_STATE" }, log("debug", "Disconnected, persisting state"));
|
|
1748
1804
|
if (state.mode === "conflict_resolution") {
|
|
1749
|
-
const { pendingConflicts: _discarded
|
|
1805
|
+
const { pendingConflicts: _discarded, ...rest } = state;
|
|
1750
1806
|
return {
|
|
1751
1807
|
state: {
|
|
1752
1808
|
...rest,
|
|
@@ -1764,8 +1820,7 @@ function transition(state, event) {
|
|
|
1764
1820
|
},
|
|
1765
1821
|
effects
|
|
1766
1822
|
};
|
|
1767
|
-
|
|
1768
|
-
case "REQUEST_FILES": {
|
|
1823
|
+
case "REQUEST_FILES":
|
|
1769
1824
|
if (state.mode === "disconnected") {
|
|
1770
1825
|
effects.push(log("warn", "Received REQUEST_FILES while disconnected, ignoring"));
|
|
1771
1826
|
return {
|
|
@@ -1778,8 +1833,7 @@ function transition(state, event) {
|
|
|
1778
1833
|
state,
|
|
1779
1834
|
effects
|
|
1780
1835
|
};
|
|
1781
|
-
|
|
1782
|
-
case "FILE_LIST": {
|
|
1836
|
+
case "FILE_LIST":
|
|
1783
1837
|
if (state.mode !== "handshaking") {
|
|
1784
1838
|
effects.push(log("warn", `Received FILE_LIST in mode ${state.mode}, ignoring`));
|
|
1785
1839
|
return {
|
|
@@ -1800,7 +1854,6 @@ function transition(state, event) {
|
|
|
1800
1854
|
},
|
|
1801
1855
|
effects
|
|
1802
1856
|
};
|
|
1803
|
-
}
|
|
1804
1857
|
case "CONFLICTS_DETECTED": {
|
|
1805
1858
|
if (state.mode !== "snapshot_processing") {
|
|
1806
1859
|
effects.push(log("warn", `Received CONFLICTS_DETECTED in mode ${state.mode}, ignoring`));
|
|
@@ -1840,7 +1893,6 @@ function transition(state, event) {
|
|
|
1840
1893
|
effects
|
|
1841
1894
|
};
|
|
1842
1895
|
}
|
|
1843
|
-
const totalSynced = safeWrites.length + localOnly.length;
|
|
1844
1896
|
const remoteTotal = state.queuedDiffs.length;
|
|
1845
1897
|
const totalCount = remoteTotal + localOnly.length;
|
|
1846
1898
|
const updatedCount = safeWrites.length + localOnly.length;
|
|
@@ -1881,14 +1933,15 @@ function transition(state, event) {
|
|
|
1881
1933
|
}
|
|
1882
1934
|
effects.push(log("debug", `Applying remote change: ${event.file.name}`), {
|
|
1883
1935
|
type: "WRITE_FILES",
|
|
1884
|
-
files: [event.file]
|
|
1936
|
+
files: [event.file],
|
|
1937
|
+
skipEcho: true
|
|
1885
1938
|
});
|
|
1886
1939
|
return {
|
|
1887
1940
|
state,
|
|
1888
1941
|
effects
|
|
1889
1942
|
};
|
|
1890
1943
|
}
|
|
1891
|
-
case "REMOTE_FILE_DELETE":
|
|
1944
|
+
case "REMOTE_FILE_DELETE":
|
|
1892
1945
|
if (state.mode === "disconnected") {
|
|
1893
1946
|
effects.push(log("warn", `Rejected delete while disconnected: ${event.fileName}`));
|
|
1894
1947
|
return {
|
|
@@ -1904,8 +1957,7 @@ function transition(state, event) {
|
|
|
1904
1957
|
state,
|
|
1905
1958
|
effects
|
|
1906
1959
|
};
|
|
1907
|
-
|
|
1908
|
-
case "REMOTE_DELETE_CONFIRMED": {
|
|
1960
|
+
case "REMOTE_DELETE_CONFIRMED":
|
|
1909
1961
|
effects.push(log("debug", `Delete confirmed: ${event.fileName}`), {
|
|
1910
1962
|
type: "DELETE_LOCAL_FILES",
|
|
1911
1963
|
names: [event.fileName]
|
|
@@ -1914,8 +1966,7 @@ function transition(state, event) {
|
|
|
1914
1966
|
state,
|
|
1915
1967
|
effects
|
|
1916
1968
|
};
|
|
1917
|
-
|
|
1918
|
-
case "REMOTE_DELETE_CANCELLED": {
|
|
1969
|
+
case "REMOTE_DELETE_CANCELLED":
|
|
1919
1970
|
effects.push(log("debug", `Delete cancelled: ${event.fileName}`));
|
|
1920
1971
|
effects.push({
|
|
1921
1972
|
type: "WRITE_FILES",
|
|
@@ -1929,7 +1980,6 @@ function transition(state, event) {
|
|
|
1929
1980
|
state,
|
|
1930
1981
|
effects
|
|
1931
1982
|
};
|
|
1932
|
-
}
|
|
1933
1983
|
case "CONFLICTS_RESOLVED": {
|
|
1934
1984
|
if (state.mode !== "conflict_resolution") {
|
|
1935
1985
|
effects.push(log("warn", `Received CONFLICTS_RESOLVED in mode ${state.mode}, ignoring`));
|
|
@@ -1976,7 +2026,7 @@ function transition(state, event) {
|
|
|
1976
2026
|
updatedCount: state.pendingConflicts.length,
|
|
1977
2027
|
unchangedCount: 0
|
|
1978
2028
|
});
|
|
1979
|
-
const { pendingConflicts: _discarded
|
|
2029
|
+
const { pendingConflicts: _discarded, ...rest } = state;
|
|
1980
2030
|
return {
|
|
1981
2031
|
state: {
|
|
1982
2032
|
...rest,
|
|
@@ -1996,7 +2046,7 @@ function transition(state, event) {
|
|
|
1996
2046
|
}
|
|
1997
2047
|
switch (kind) {
|
|
1998
2048
|
case "add":
|
|
1999
|
-
case "change":
|
|
2049
|
+
case "change":
|
|
2000
2050
|
if (content === void 0) {
|
|
2001
2051
|
effects.push(log("warn", `Watcher event missing content: ${relativePath}`));
|
|
2002
2052
|
return {
|
|
@@ -2010,15 +2060,13 @@ function transition(state, event) {
|
|
|
2010
2060
|
content
|
|
2011
2061
|
});
|
|
2012
2062
|
break;
|
|
2013
|
-
|
|
2014
|
-
case "delete": {
|
|
2063
|
+
case "delete":
|
|
2015
2064
|
effects.push(log("debug", `Local delete detected: ${relativePath}`), {
|
|
2016
2065
|
type: "REQUEST_LOCAL_DELETE_DECISION",
|
|
2017
2066
|
fileName: relativePath,
|
|
2018
2067
|
requireConfirmation: true
|
|
2019
2068
|
});
|
|
2020
2069
|
break;
|
|
2021
|
-
}
|
|
2022
2070
|
}
|
|
2023
2071
|
return {
|
|
2024
2072
|
state,
|
|
@@ -2084,7 +2132,7 @@ function transition(state, event) {
|
|
|
2084
2132
|
updatedCount: resolvedCount,
|
|
2085
2133
|
unchangedCount: 0
|
|
2086
2134
|
});
|
|
2087
|
-
const { pendingConflicts: _discarded
|
|
2135
|
+
const { pendingConflicts: _discarded, ...rest } = state;
|
|
2088
2136
|
return {
|
|
2089
2137
|
state: {
|
|
2090
2138
|
...rest,
|
|
@@ -2094,13 +2142,12 @@ function transition(state, event) {
|
|
|
2094
2142
|
effects
|
|
2095
2143
|
};
|
|
2096
2144
|
}
|
|
2097
|
-
default:
|
|
2145
|
+
default:
|
|
2098
2146
|
effects.push(log("warn", `Unhandled event type in transition`));
|
|
2099
2147
|
return {
|
|
2100
2148
|
state,
|
|
2101
2149
|
effects
|
|
2102
2150
|
};
|
|
2103
|
-
}
|
|
2104
2151
|
}
|
|
2105
2152
|
}
|
|
2106
2153
|
/**
|
|
@@ -2110,7 +2157,7 @@ function transition(state, event) {
|
|
|
2110
2157
|
async function executeEffect(effect, context) {
|
|
2111
2158
|
const { config, hashTracker, installer, fileMetadataCache, userActions, syncState } = context;
|
|
2112
2159
|
switch (effect.type) {
|
|
2113
|
-
case "INIT_WORKSPACE":
|
|
2160
|
+
case "INIT_WORKSPACE":
|
|
2114
2161
|
if (!config.projectDir) {
|
|
2115
2162
|
const projectName = config.explicitName ?? effect.projectInfo.projectName;
|
|
2116
2163
|
config.projectDir = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir);
|
|
@@ -2119,14 +2166,12 @@ async function executeEffect(effect, context) {
|
|
|
2119
2166
|
await fs.mkdir(config.filesDir, { recursive: true });
|
|
2120
2167
|
}
|
|
2121
2168
|
return [];
|
|
2122
|
-
|
|
2123
|
-
case "LOAD_PERSISTED_STATE": {
|
|
2169
|
+
case "LOAD_PERSISTED_STATE":
|
|
2124
2170
|
if (config.projectDir) {
|
|
2125
2171
|
await fileMetadataCache.initialize(config.projectDir);
|
|
2126
2172
|
debug(`Loaded persisted metadata for ${fileMetadataCache.size()} files`);
|
|
2127
2173
|
}
|
|
2128
2174
|
return [];
|
|
2129
|
-
}
|
|
2130
2175
|
case "LIST_LOCAL_FILES": {
|
|
2131
2176
|
if (!config.filesDir) return [];
|
|
2132
2177
|
const files = await listFiles(config.filesDir);
|
|
@@ -2138,7 +2183,8 @@ async function executeEffect(effect, context) {
|
|
|
2138
2183
|
}
|
|
2139
2184
|
case "DETECT_CONFLICTS": {
|
|
2140
2185
|
if (!config.filesDir) return [];
|
|
2141
|
-
const { conflicts, writes, localOnly } = await detectConflicts(effect.remoteFiles, config.filesDir, { persistedState: fileMetadataCache.getPersistedState() });
|
|
2186
|
+
const { conflicts, writes, localOnly, unchanged } = await detectConflicts(effect.remoteFiles, config.filesDir, { persistedState: fileMetadataCache.getPersistedState() });
|
|
2187
|
+
for (const file of unchanged) fileMetadataCache.recordRemoteWrite(file.name, file.content, file.modifiedAt ?? Date.now());
|
|
2142
2188
|
return [{
|
|
2143
2189
|
type: "CONFLICTS_DETECTED",
|
|
2144
2190
|
conflicts,
|
|
@@ -2146,36 +2192,34 @@ async function executeEffect(effect, context) {
|
|
|
2146
2192
|
localOnly
|
|
2147
2193
|
}];
|
|
2148
2194
|
}
|
|
2149
|
-
case "SEND_MESSAGE":
|
|
2195
|
+
case "SEND_MESSAGE":
|
|
2150
2196
|
if (syncState.socket) {
|
|
2151
|
-
|
|
2152
|
-
if (!sent) warn(`Failed to send message: ${effect.payload.type}`);
|
|
2197
|
+
if (!await sendMessage(syncState.socket, effect.payload)) warn(`Failed to send message: ${effect.payload.type}`);
|
|
2153
2198
|
} else warn(`No socket available to send: ${effect.payload.type}`);
|
|
2154
2199
|
return [];
|
|
2155
|
-
|
|
2156
|
-
case "WRITE_FILES": {
|
|
2200
|
+
case "WRITE_FILES":
|
|
2157
2201
|
if (config.filesDir) {
|
|
2158
|
-
|
|
2159
|
-
|
|
2202
|
+
const filesToWrite = effect.skipEcho === true ? filterEchoedFiles(effect.files, hashTracker) : effect.files;
|
|
2203
|
+
if (effect.skipEcho && filesToWrite.length !== effect.files.length) debug(`Skipped ${pluralize(effect.files.length - filesToWrite.length, "echoed change")}`);
|
|
2204
|
+
if (filesToWrite.length === 0) return [];
|
|
2205
|
+
await writeRemoteFiles(filesToWrite, config.filesDir, hashTracker, installer ?? void 0);
|
|
2206
|
+
for (const file of filesToWrite) {
|
|
2160
2207
|
if (!effect.silent) fileDown(file.name);
|
|
2161
2208
|
const remoteTimestamp = file.modifiedAt ?? Date.now();
|
|
2162
2209
|
fileMetadataCache.recordRemoteWrite(file.name, file.content, remoteTimestamp);
|
|
2163
2210
|
}
|
|
2164
2211
|
}
|
|
2165
2212
|
return [];
|
|
2166
|
-
|
|
2167
|
-
case "DELETE_LOCAL_FILES": {
|
|
2213
|
+
case "DELETE_LOCAL_FILES":
|
|
2168
2214
|
if (config.filesDir) for (const fileName of effect.names) {
|
|
2169
2215
|
await deleteLocalFile(fileName, config.filesDir, hashTracker);
|
|
2170
2216
|
fileDelete(fileName);
|
|
2171
2217
|
fileMetadataCache.recordDelete(fileName);
|
|
2172
2218
|
}
|
|
2173
2219
|
return [];
|
|
2174
|
-
|
|
2175
|
-
case "REQUEST_CONFLICT_DECISIONS": {
|
|
2220
|
+
case "REQUEST_CONFLICT_DECISIONS":
|
|
2176
2221
|
await userActions.requestConflictDecisions(syncState.socket, effect.conflicts);
|
|
2177
2222
|
return [];
|
|
2178
|
-
}
|
|
2179
2223
|
case "REQUEST_CONFLICT_VERSIONS": {
|
|
2180
2224
|
if (!syncState.socket) {
|
|
2181
2225
|
warn("Cannot request conflict versions without active socket");
|
|
@@ -2196,14 +2240,13 @@ async function executeEffect(effect, context) {
|
|
|
2196
2240
|
});
|
|
2197
2241
|
return [];
|
|
2198
2242
|
}
|
|
2199
|
-
case "REQUEST_DELETE_CONFIRMATION":
|
|
2243
|
+
case "REQUEST_DELETE_CONFIRMATION":
|
|
2200
2244
|
if (syncState.socket) await sendMessage(syncState.socket, {
|
|
2201
2245
|
type: "file-delete",
|
|
2202
2246
|
fileNames: [effect.fileName],
|
|
2203
2247
|
requireConfirmation: effect.requireConfirmation
|
|
2204
2248
|
});
|
|
2205
2249
|
return [];
|
|
2206
|
-
}
|
|
2207
2250
|
case "UPDATE_FILE_METADATA": {
|
|
2208
2251
|
if (!config.filesDir || !config.projectDir) return [];
|
|
2209
2252
|
const currentContent = await readFileSafe(effect.fileName, config.filesDir);
|
|
@@ -2215,8 +2258,7 @@ async function executeEffect(effect, context) {
|
|
|
2215
2258
|
}
|
|
2216
2259
|
case "SEND_LOCAL_CHANGE": {
|
|
2217
2260
|
const contentHash = hashFileContent(effect.content);
|
|
2218
|
-
|
|
2219
|
-
if (metadata?.lastSyncedHash === contentHash) {
|
|
2261
|
+
if (fileMetadataCache.get(effect.fileName)?.lastSyncedHash === contentHash) {
|
|
2220
2262
|
debug(`Skipping local change for ${effect.fileName}: matches last synced content`);
|
|
2221
2263
|
return [];
|
|
2222
2264
|
}
|
|
@@ -2238,18 +2280,16 @@ async function executeEffect(effect, context) {
|
|
|
2238
2280
|
}
|
|
2239
2281
|
return [];
|
|
2240
2282
|
}
|
|
2241
|
-
case "REQUEST_LOCAL_DELETE_DECISION":
|
|
2242
|
-
|
|
2243
|
-
if (shouldSkip) {
|
|
2283
|
+
case "REQUEST_LOCAL_DELETE_DECISION":
|
|
2284
|
+
if (hashTracker.shouldSkipDelete(effect.fileName)) {
|
|
2244
2285
|
hashTracker.clearDelete(effect.fileName);
|
|
2245
2286
|
return [];
|
|
2246
2287
|
}
|
|
2247
2288
|
try {
|
|
2248
|
-
|
|
2289
|
+
if (await userActions.requestDeleteDecision(syncState.socket, {
|
|
2249
2290
|
fileName: effect.fileName,
|
|
2250
2291
|
requireConfirmation: !config.dangerouslyAutoDelete
|
|
2251
|
-
})
|
|
2252
|
-
if (shouldDelete) {
|
|
2292
|
+
})) {
|
|
2253
2293
|
hashTracker.forget(effect.fileName);
|
|
2254
2294
|
fileMetadataCache.recordDelete(effect.fileName);
|
|
2255
2295
|
if (syncState.socket) await sendMessage(syncState.socket, {
|
|
@@ -2261,13 +2301,12 @@ async function executeEffect(effect, context) {
|
|
|
2261
2301
|
console.warn(`Failed to handle deletion for ${effect.fileName}:`, err);
|
|
2262
2302
|
}
|
|
2263
2303
|
return [];
|
|
2264
|
-
|
|
2265
|
-
case "PERSIST_STATE": {
|
|
2304
|
+
case "PERSIST_STATE":
|
|
2266
2305
|
await fileMetadataCache.flush();
|
|
2267
2306
|
return [];
|
|
2268
|
-
}
|
|
2269
2307
|
case "SYNC_COMPLETE": {
|
|
2270
2308
|
const wasDisconnected = wasRecentlyDisconnected();
|
|
2309
|
+
if (syncState.socket) await sendMessage(syncState.socket, { type: "sync-complete" });
|
|
2271
2310
|
if (wasDisconnected) {
|
|
2272
2311
|
if (didShowDisconnect()) {
|
|
2273
2312
|
success(`Reconnected, synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
@@ -2280,11 +2319,9 @@ async function executeEffect(effect, context) {
|
|
|
2280
2319
|
status("Watching for changes...");
|
|
2281
2320
|
return [];
|
|
2282
2321
|
}
|
|
2283
|
-
case "LOG":
|
|
2284
|
-
|
|
2285
|
-
logFn(effect.message);
|
|
2322
|
+
case "LOG":
|
|
2323
|
+
(effect.level === "info" ? info : effect.level === "warn" ? warn : debug)(effect.message);
|
|
2286
2324
|
return [];
|
|
2287
|
-
}
|
|
2288
2325
|
}
|
|
2289
2326
|
}
|
|
2290
2327
|
/**
|
|
@@ -2347,8 +2384,7 @@ async function start(config) {
|
|
|
2347
2384
|
startWatcher();
|
|
2348
2385
|
}
|
|
2349
2386
|
cancelDisconnectMessage();
|
|
2350
|
-
|
|
2351
|
-
if (!wasDisconnected && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2387
|
+
if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2352
2388
|
});
|
|
2353
2389
|
async function handleMessage(message) {
|
|
2354
2390
|
if (!config.projectDir || !installer) {
|
|
@@ -2380,26 +2416,22 @@ async function start(config) {
|
|
|
2380
2416
|
fileMeta: fileMetadataCache.get(message.fileName)
|
|
2381
2417
|
};
|
|
2382
2418
|
break;
|
|
2383
|
-
case "file-delete":
|
|
2419
|
+
case "file-delete":
|
|
2384
2420
|
for (const fileName of message.fileNames) await processEvent({
|
|
2385
2421
|
type: "REMOTE_FILE_DELETE",
|
|
2386
2422
|
fileName
|
|
2387
2423
|
});
|
|
2388
2424
|
return;
|
|
2389
|
-
}
|
|
2390
2425
|
case "delete-confirmed": {
|
|
2391
2426
|
const unmatched = [];
|
|
2392
|
-
for (const fileName of message.fileNames) {
|
|
2393
|
-
const handled = userActions.handleConfirmation(`delete:${fileName}`, true);
|
|
2394
|
-
if (!handled) unmatched.push(fileName);
|
|
2395
|
-
}
|
|
2427
|
+
for (const fileName of message.fileNames) if (!userActions.handleConfirmation(`delete:${fileName}`, true)) unmatched.push(fileName);
|
|
2396
2428
|
for (const fileName of unmatched) await processEvent({
|
|
2397
2429
|
type: "REMOTE_DELETE_CONFIRMED",
|
|
2398
2430
|
fileName
|
|
2399
2431
|
});
|
|
2400
2432
|
return;
|
|
2401
2433
|
}
|
|
2402
|
-
case "delete-cancelled":
|
|
2434
|
+
case "delete-cancelled":
|
|
2403
2435
|
for (const file of message.files) {
|
|
2404
2436
|
userActions.handleConfirmation(`delete:${file.fileName}`, false);
|
|
2405
2437
|
await processEvent({
|
|
@@ -2409,7 +2441,6 @@ async function start(config) {
|
|
|
2409
2441
|
});
|
|
2410
2442
|
}
|
|
2411
2443
|
return;
|
|
2412
|
-
}
|
|
2413
2444
|
case "file-synced":
|
|
2414
2445
|
event = {
|
|
2415
2446
|
type: "FILE_SYNCED",
|
|
@@ -2474,6 +2505,12 @@ async function start(config) {
|
|
|
2474
2505
|
|
|
2475
2506
|
//#endregion
|
|
2476
2507
|
//#region src/index.ts
|
|
2508
|
+
/**
|
|
2509
|
+
* Framer Code Link CLI - Next Generation
|
|
2510
|
+
*
|
|
2511
|
+
* Entry point for the CLI tool. Parses command-line arguments and starts
|
|
2512
|
+
* the controller with the appropriate configuration.
|
|
2513
|
+
*/
|
|
2477
2514
|
const program = new Command();
|
|
2478
2515
|
program.exitOverride((err) => {
|
|
2479
2516
|
if (err.code === "commander.missingArgument") {
|
|
@@ -2494,13 +2531,12 @@ program.name("code-link").description("Sync Framer code components to your local
|
|
|
2494
2531
|
}
|
|
2495
2532
|
const isDev = process.env.NODE_ENV === "development";
|
|
2496
2533
|
if (options.logLevel) {
|
|
2497
|
-
const
|
|
2534
|
+
const level = {
|
|
2498
2535
|
debug: LogLevel.DEBUG,
|
|
2499
2536
|
info: LogLevel.INFO,
|
|
2500
2537
|
warn: LogLevel.WARN,
|
|
2501
2538
|
error: LogLevel.ERROR
|
|
2502
|
-
};
|
|
2503
|
-
const level = levelMap[options.logLevel.toLowerCase()];
|
|
2539
|
+
}[options.logLevel.toLowerCase()];
|
|
2504
2540
|
if (level !== void 0) setLogLevel(level);
|
|
2505
2541
|
} else if (options.verbose || isDev) setLogLevel(LogLevel.DEBUG);
|
|
2506
2542
|
const port = getPortFromHash(projectHash);
|