framer-code-link 0.15.0 → 0.17.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 +79 -25
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -509,6 +509,7 @@ function initConnection(port) {
|
|
|
509
509
|
wss.on("listening", () => {
|
|
510
510
|
isReady = true;
|
|
511
511
|
debug(`WebSocket server listening on port ${port}`);
|
|
512
|
+
let activeClient = null;
|
|
512
513
|
wss.on("connection", (ws) => {
|
|
513
514
|
const connId = ++connectionId;
|
|
514
515
|
let handshakeReceived = false;
|
|
@@ -519,8 +520,15 @@ function initConnection(port) {
|
|
|
519
520
|
if (message.type === "handshake") {
|
|
520
521
|
debug(`Received handshake (conn ${connId})`);
|
|
521
522
|
handshakeReceived = true;
|
|
523
|
+
const previousActiveClient = activeClient;
|
|
524
|
+
activeClient = ws;
|
|
525
|
+
if (previousActiveClient && previousActiveClient !== activeClient) {
|
|
526
|
+
debug(`Replacing active client with conn ${connId}`);
|
|
527
|
+
if (previousActiveClient.readyState === READY_STATE.OPEN || previousActiveClient.readyState === READY_STATE.CONNECTING) previousActiveClient.close();
|
|
528
|
+
}
|
|
522
529
|
handlers.onHandshake?.(ws, message);
|
|
523
|
-
} else if (handshakeReceived) handlers.onMessage?.(message);
|
|
530
|
+
} else if (handshakeReceived && activeClient === ws) handlers.onMessage?.(message);
|
|
531
|
+
else if (handshakeReceived) debug(`Ignoring ${message.type} from stale client (conn ${connId})`);
|
|
524
532
|
else debug(`Ignoring ${message.type} before handshake (conn ${connId})`);
|
|
525
533
|
} catch (err) {
|
|
526
534
|
error(`Failed to parse message:`, err);
|
|
@@ -528,7 +536,10 @@ function initConnection(port) {
|
|
|
528
536
|
});
|
|
529
537
|
ws.on("close", (code, reason) => {
|
|
530
538
|
debug(`Client disconnected (code: ${code}, reason: ${reason.toString()})`);
|
|
531
|
-
|
|
539
|
+
if (activeClient === ws) {
|
|
540
|
+
activeClient = null;
|
|
541
|
+
handlers.onDisconnect?.(ws);
|
|
542
|
+
} else debug(`Ignoring disconnect from stale client (conn ${connId})`);
|
|
532
543
|
});
|
|
533
544
|
ws.on("error", (err) => {
|
|
534
545
|
error(`WebSocket error:`, err);
|
|
@@ -680,7 +691,7 @@ async function loadPersistedState(projectDir) {
|
|
|
680
691
|
if (normalizedName !== fileName) debug(`Normalized persisted key "${fileName}" -> "${normalizedName}" for compatibility`);
|
|
681
692
|
result.set(normalizedName, state);
|
|
682
693
|
}
|
|
683
|
-
debug(`Loaded persisted state for ${result.size}
|
|
694
|
+
debug(`Loaded persisted state for ${pluralize(result.size, "file")}`);
|
|
684
695
|
return result;
|
|
685
696
|
} catch (err) {
|
|
686
697
|
if (err.code === "ENOENT") {
|
|
@@ -702,7 +713,7 @@ async function savePersistedState(projectDir, state) {
|
|
|
702
713
|
};
|
|
703
714
|
try {
|
|
704
715
|
await fs.writeFile(statePath, JSON.stringify(persistedState, null, 2));
|
|
705
|
-
debug(`Saved persisted state for ${state.size}
|
|
716
|
+
debug(`Saved persisted state for ${pluralize(state.size, "file")}`);
|
|
706
717
|
} catch (err) {
|
|
707
718
|
warn("Failed to save persisted state:", err);
|
|
708
719
|
}
|
|
@@ -773,7 +784,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
773
784
|
const preferRemote = options.preferRemote ?? false;
|
|
774
785
|
const persistedState = options.persistedState;
|
|
775
786
|
const getPersistedState = (fileName) => persistedState?.get(fileKeyForLookup(fileName));
|
|
776
|
-
debug(`Detecting conflicts for ${
|
|
787
|
+
debug(`Detecting conflicts for ${pluralize(remoteFiles.length, "remote file")}`);
|
|
777
788
|
const localFiles = await listFiles(filesDir);
|
|
778
789
|
const localFileMap = new Map(localFiles.map((f) => [fileKeyForLookup(f.name), f]));
|
|
779
790
|
const remoteFileMap = new Map(remoteFiles.map((f) => {
|
|
@@ -927,7 +938,7 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
927
938
|
* CRITICAL: Update hashTracker BEFORE writing to disk
|
|
928
939
|
*/
|
|
929
940
|
async function writeRemoteFiles(files, filesDir, hashTracker, installer) {
|
|
930
|
-
debug(`Writing ${files.length
|
|
941
|
+
debug(`Writing ${pluralize(files.length, "remote file")}`);
|
|
931
942
|
for (const file of files) try {
|
|
932
943
|
const normalized = resolveRemoteReference(filesDir, file.name);
|
|
933
944
|
const fullPath = normalized.absolutePath;
|
|
@@ -1994,7 +2005,8 @@ async function getProjectHashFromCwd() {
|
|
|
1994
2005
|
return null;
|
|
1995
2006
|
}
|
|
1996
2007
|
}
|
|
1997
|
-
async function findOrCreateProjectDirectory(
|
|
2008
|
+
async function findOrCreateProjectDirectory(options) {
|
|
2009
|
+
const { projectHash, projectName, explicitDirectory, baseDirectory } = options;
|
|
1998
2010
|
if (explicitDirectory) {
|
|
1999
2011
|
const resolved = path.resolve(explicitDirectory);
|
|
2000
2012
|
await fs.mkdir(path.join(resolved, "files"), { recursive: true });
|
|
@@ -2003,7 +2015,7 @@ async function findOrCreateProjectDirectory(projectHash, projectName, explicitDi
|
|
|
2003
2015
|
created: false
|
|
2004
2016
|
};
|
|
2005
2017
|
}
|
|
2006
|
-
const cwd = process.cwd();
|
|
2018
|
+
const cwd = baseDirectory ?? process.cwd();
|
|
2007
2019
|
const existing = await findExistingProjectDirectory(cwd, projectHash);
|
|
2008
2020
|
if (existing) return {
|
|
2009
2021
|
directory: existing,
|
|
@@ -2013,7 +2025,7 @@ async function findOrCreateProjectDirectory(projectHash, projectName, explicitDi
|
|
|
2013
2025
|
const directoryName = toDirectoryName(projectName);
|
|
2014
2026
|
const pkgName = toPackageName(projectName);
|
|
2015
2027
|
const shortId = shortProjectHash(projectHash);
|
|
2016
|
-
const projectDirectory =
|
|
2028
|
+
const { directory: projectDirectory, nameCollision } = await findAvailableDirectory(cwd, directoryName || `project-${shortId}`, shortId);
|
|
2017
2029
|
await fs.mkdir(path.join(projectDirectory, "files"), { recursive: true });
|
|
2018
2030
|
const pkg = {
|
|
2019
2031
|
name: pkgName || shortId,
|
|
@@ -2025,9 +2037,29 @@ async function findOrCreateProjectDirectory(projectHash, projectName, explicitDi
|
|
|
2025
2037
|
await fs.writeFile(path.join(projectDirectory, "package.json"), JSON.stringify(pkg, null, 2));
|
|
2026
2038
|
return {
|
|
2027
2039
|
directory: projectDirectory,
|
|
2028
|
-
created: true
|
|
2040
|
+
created: true,
|
|
2041
|
+
nameCollision
|
|
2029
2042
|
};
|
|
2030
2043
|
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Returns a directory path that doesn't collide with an existing project.
|
|
2046
|
+
* Tries the bare name first, falls back to name-{shortId} if taken.
|
|
2047
|
+
*/
|
|
2048
|
+
async function findAvailableDirectory(baseDir, name, shortId) {
|
|
2049
|
+
const candidate = path.join(baseDir, name);
|
|
2050
|
+
try {
|
|
2051
|
+
await fs.access(candidate);
|
|
2052
|
+
return {
|
|
2053
|
+
directory: path.join(baseDir, `${name}-${shortId}`),
|
|
2054
|
+
nameCollision: true
|
|
2055
|
+
};
|
|
2056
|
+
} catch {
|
|
2057
|
+
return {
|
|
2058
|
+
directory: candidate,
|
|
2059
|
+
nameCollision: false
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2031
2063
|
async function findExistingProjectDirectory(baseDirectory, projectHash) {
|
|
2032
2064
|
if (await matchesProject(path.join(baseDirectory, "package.json"), projectHash)) return baseDirectory;
|
|
2033
2065
|
const entries = await fs.readdir(baseDirectory, { withFileTypes: true });
|
|
@@ -2141,7 +2173,7 @@ function transition(state, event) {
|
|
|
2141
2173
|
effects
|
|
2142
2174
|
};
|
|
2143
2175
|
}
|
|
2144
|
-
effects.push(log("debug", `Received file list: ${event.files.length}
|
|
2176
|
+
effects.push(log("debug", `Received file list: ${pluralize(event.files.length, "file")}`));
|
|
2145
2177
|
effects.push({
|
|
2146
2178
|
type: "DETECT_CONFLICTS",
|
|
2147
2179
|
remoteFiles: event.files
|
|
@@ -2163,13 +2195,17 @@ function transition(state, event) {
|
|
|
2163
2195
|
};
|
|
2164
2196
|
}
|
|
2165
2197
|
const { conflicts, safeWrites, localOnly } = event;
|
|
2166
|
-
if (safeWrites.length > 0)
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2198
|
+
if (safeWrites.length > 0) {
|
|
2199
|
+
effects.push(log("debug", `Applying ${safeWrites.length} safe writes`));
|
|
2200
|
+
if (wasRecentlyDisconnected()) effects.push(log("success", `Applied ${pluralize(safeWrites.length, "file")} during sync`));
|
|
2201
|
+
effects.push({
|
|
2202
|
+
type: "WRITE_FILES",
|
|
2203
|
+
files: safeWrites,
|
|
2204
|
+
silent: true
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2171
2207
|
if (localOnly.length > 0) {
|
|
2172
|
-
effects.push(log("debug", `Uploading ${localOnly.length
|
|
2208
|
+
effects.push(log("debug", `Uploading ${pluralize(localOnly.length, "local-only file")}`));
|
|
2173
2209
|
for (const file of localOnly) effects.push({
|
|
2174
2210
|
type: "SEND_MESSAGE",
|
|
2175
2211
|
payload: {
|
|
@@ -2456,9 +2492,14 @@ async function executeEffect(effect, context) {
|
|
|
2456
2492
|
case "INIT_WORKSPACE":
|
|
2457
2493
|
if (!config.projectDir) {
|
|
2458
2494
|
const projectName = config.explicitName ?? effect.projectInfo.projectName;
|
|
2459
|
-
const directoryInfo = await findOrCreateProjectDirectory(
|
|
2495
|
+
const directoryInfo = await findOrCreateProjectDirectory({
|
|
2496
|
+
projectHash: config.projectHash,
|
|
2497
|
+
projectName,
|
|
2498
|
+
explicitDirectory: config.explicitDirectory
|
|
2499
|
+
});
|
|
2460
2500
|
config.projectDir = directoryInfo.directory;
|
|
2461
2501
|
config.projectDirCreated = directoryInfo.created;
|
|
2502
|
+
if (directoryInfo.nameCollision) warn(`Folder ${projectName} already exists`);
|
|
2462
2503
|
config.filesDir = `${config.projectDir}/files`;
|
|
2463
2504
|
debug(`Files directory: ${config.filesDir}`);
|
|
2464
2505
|
await fs.mkdir(config.filesDir, { recursive: true });
|
|
@@ -2467,7 +2508,7 @@ async function executeEffect(effect, context) {
|
|
|
2467
2508
|
case "LOAD_PERSISTED_STATE":
|
|
2468
2509
|
if (config.projectDir) {
|
|
2469
2510
|
await fileMetadataCache.initialize(config.projectDir);
|
|
2470
|
-
debug(`Loaded persisted metadata for ${fileMetadataCache.size()}
|
|
2511
|
+
debug(`Loaded persisted metadata for ${pluralize(fileMetadataCache.size(), "file")}`);
|
|
2471
2512
|
}
|
|
2472
2513
|
return [];
|
|
2473
2514
|
case "LIST_LOCAL_FILES": {
|
|
@@ -2586,6 +2627,7 @@ async function executeEffect(effect, context) {
|
|
|
2586
2627
|
for (const fileName of confirmedFiles) {
|
|
2587
2628
|
hashTracker.forget(fileName);
|
|
2588
2629
|
fileMetadataCache.recordDelete(fileName);
|
|
2630
|
+
fileDelete(fileName);
|
|
2589
2631
|
}
|
|
2590
2632
|
if (confirmedFiles.length > 0 && syncState.socket) await sendMessage(syncState.socket, {
|
|
2591
2633
|
type: "file-delete",
|
|
@@ -2604,7 +2646,7 @@ async function executeEffect(effect, context) {
|
|
|
2604
2646
|
if (syncState.socket) await sendMessage(syncState.socket, { type: "sync-complete" });
|
|
2605
2647
|
if (wasDisconnected) {
|
|
2606
2648
|
if (didShowDisconnect()) {
|
|
2607
|
-
success(`Reconnected, synced ${effect.totalCount}
|
|
2649
|
+
success(`Reconnected, synced ${pluralize(effect.totalCount, "file")} (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2608
2650
|
status("Watching for changes...");
|
|
2609
2651
|
}
|
|
2610
2652
|
resetDisconnectState();
|
|
@@ -2614,9 +2656,9 @@ async function executeEffect(effect, context) {
|
|
|
2614
2656
|
const relativeDirectory = relative != null ? relative ? "./" + relative : "." : null;
|
|
2615
2657
|
if (effect.totalCount === 0 && relativeDirectory) if (config.projectDirCreated) success(`Created ${relativeDirectory} folder`);
|
|
2616
2658
|
else success(`Syncing to ${relativeDirectory} folder`);
|
|
2617
|
-
else if (relativeDirectory && config.projectDirCreated) success(`
|
|
2618
|
-
else if (relativeDirectory) success(`Synced into ${relativeDirectory} (${effect.updatedCount}
|
|
2619
|
-
else success(`Synced ${effect.totalCount}
|
|
2659
|
+
else if (relativeDirectory && config.projectDirCreated) success(`Created ${relativeDirectory} (${pluralize(effect.updatedCount, "file")} added)`);
|
|
2660
|
+
else if (relativeDirectory) success(`Synced into ${relativeDirectory} (${pluralize(effect.updatedCount, "file")} updated, ${effect.unchangedCount} unchanged)`);
|
|
2661
|
+
else success(`Synced ${pluralize(effect.totalCount, "file")} (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2620
2662
|
if (config.projectDirCreated && config.projectDir) tryGitInit(config.projectDir);
|
|
2621
2663
|
status("Watching for changes...");
|
|
2622
2664
|
return [];
|
|
@@ -2679,6 +2721,14 @@ async function start(config) {
|
|
|
2679
2721
|
}
|
|
2680
2722
|
(async () => {
|
|
2681
2723
|
cancelDisconnectMessage();
|
|
2724
|
+
if (syncState.mode !== "disconnected") {
|
|
2725
|
+
if (syncState.socket === client) {
|
|
2726
|
+
debug(`Ignoring duplicate handshake from active socket in ${syncState.mode} mode`);
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
debug(`New handshake received in ${syncState.mode} mode, resetting sync state`);
|
|
2730
|
+
await processEvent({ type: "DISCONNECT" });
|
|
2731
|
+
}
|
|
2682
2732
|
if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2683
2733
|
await processEvent({
|
|
2684
2734
|
type: "HANDSHAKE",
|
|
@@ -2709,7 +2759,7 @@ async function start(config) {
|
|
|
2709
2759
|
event = { type: "REQUEST_FILES" };
|
|
2710
2760
|
break;
|
|
2711
2761
|
case "file-list":
|
|
2712
|
-
debug(`Received file list: ${message.files.length}
|
|
2762
|
+
debug(`Received file list: ${pluralize(message.files.length, "file")}`);
|
|
2713
2763
|
event = {
|
|
2714
2764
|
type: "REMOTE_FILE_LIST",
|
|
2715
2765
|
files: message.files
|
|
@@ -2785,7 +2835,11 @@ async function start(config) {
|
|
|
2785
2835
|
}
|
|
2786
2836
|
})();
|
|
2787
2837
|
});
|
|
2788
|
-
connection.on("disconnect", () => {
|
|
2838
|
+
connection.on("disconnect", (client) => {
|
|
2839
|
+
if (syncState.socket !== client) {
|
|
2840
|
+
debug("[STATE] Ignoring disconnect from stale socket");
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2789
2843
|
scheduleDisconnectMessage(() => {
|
|
2790
2844
|
status("Disconnected, waiting to reconnect...");
|
|
2791
2845
|
});
|