framer-code-link 0.15.0 → 0.16.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 +71 -21
- 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,13 @@ function transition(state, event) {
|
|
|
2163
2195
|
};
|
|
2164
2196
|
}
|
|
2165
2197
|
const { conflicts, safeWrites, localOnly } = event;
|
|
2166
|
-
if (safeWrites.length > 0) effects.push(log("debug", `Applying ${safeWrites.length} safe writes`), {
|
|
2198
|
+
if (safeWrites.length > 0) effects.push(log("debug", `Applying ${safeWrites.length} safe writes`), log("success", `Applied ${pluralize(safeWrites.length, "file")} during sync`), {
|
|
2167
2199
|
type: "WRITE_FILES",
|
|
2168
2200
|
files: safeWrites,
|
|
2169
2201
|
silent: true
|
|
2170
2202
|
});
|
|
2171
2203
|
if (localOnly.length > 0) {
|
|
2172
|
-
effects.push(log("debug", `Uploading ${localOnly.length
|
|
2204
|
+
effects.push(log("debug", `Uploading ${pluralize(localOnly.length, "local-only file")}`));
|
|
2173
2205
|
for (const file of localOnly) effects.push({
|
|
2174
2206
|
type: "SEND_MESSAGE",
|
|
2175
2207
|
payload: {
|
|
@@ -2456,9 +2488,14 @@ async function executeEffect(effect, context) {
|
|
|
2456
2488
|
case "INIT_WORKSPACE":
|
|
2457
2489
|
if (!config.projectDir) {
|
|
2458
2490
|
const projectName = config.explicitName ?? effect.projectInfo.projectName;
|
|
2459
|
-
const directoryInfo = await findOrCreateProjectDirectory(
|
|
2491
|
+
const directoryInfo = await findOrCreateProjectDirectory({
|
|
2492
|
+
projectHash: config.projectHash,
|
|
2493
|
+
projectName,
|
|
2494
|
+
explicitDirectory: config.explicitDirectory
|
|
2495
|
+
});
|
|
2460
2496
|
config.projectDir = directoryInfo.directory;
|
|
2461
2497
|
config.projectDirCreated = directoryInfo.created;
|
|
2498
|
+
if (directoryInfo.nameCollision) warn(`Folder ${projectName} already exists`);
|
|
2462
2499
|
config.filesDir = `${config.projectDir}/files`;
|
|
2463
2500
|
debug(`Files directory: ${config.filesDir}`);
|
|
2464
2501
|
await fs.mkdir(config.filesDir, { recursive: true });
|
|
@@ -2467,7 +2504,7 @@ async function executeEffect(effect, context) {
|
|
|
2467
2504
|
case "LOAD_PERSISTED_STATE":
|
|
2468
2505
|
if (config.projectDir) {
|
|
2469
2506
|
await fileMetadataCache.initialize(config.projectDir);
|
|
2470
|
-
debug(`Loaded persisted metadata for ${fileMetadataCache.size()}
|
|
2507
|
+
debug(`Loaded persisted metadata for ${pluralize(fileMetadataCache.size(), "file")}`);
|
|
2471
2508
|
}
|
|
2472
2509
|
return [];
|
|
2473
2510
|
case "LIST_LOCAL_FILES": {
|
|
@@ -2586,6 +2623,7 @@ async function executeEffect(effect, context) {
|
|
|
2586
2623
|
for (const fileName of confirmedFiles) {
|
|
2587
2624
|
hashTracker.forget(fileName);
|
|
2588
2625
|
fileMetadataCache.recordDelete(fileName);
|
|
2626
|
+
fileDelete(fileName);
|
|
2589
2627
|
}
|
|
2590
2628
|
if (confirmedFiles.length > 0 && syncState.socket) await sendMessage(syncState.socket, {
|
|
2591
2629
|
type: "file-delete",
|
|
@@ -2604,7 +2642,7 @@ async function executeEffect(effect, context) {
|
|
|
2604
2642
|
if (syncState.socket) await sendMessage(syncState.socket, { type: "sync-complete" });
|
|
2605
2643
|
if (wasDisconnected) {
|
|
2606
2644
|
if (didShowDisconnect()) {
|
|
2607
|
-
success(`Reconnected, synced ${effect.totalCount}
|
|
2645
|
+
success(`Reconnected, synced ${pluralize(effect.totalCount, "file")} (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2608
2646
|
status("Watching for changes...");
|
|
2609
2647
|
}
|
|
2610
2648
|
resetDisconnectState();
|
|
@@ -2614,9 +2652,9 @@ async function executeEffect(effect, context) {
|
|
|
2614
2652
|
const relativeDirectory = relative != null ? relative ? "./" + relative : "." : null;
|
|
2615
2653
|
if (effect.totalCount === 0 && relativeDirectory) if (config.projectDirCreated) success(`Created ${relativeDirectory} folder`);
|
|
2616
2654
|
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}
|
|
2655
|
+
else if (relativeDirectory && config.projectDirCreated) success(`Created ${relativeDirectory} (${pluralize(effect.updatedCount, "file")} added)`);
|
|
2656
|
+
else if (relativeDirectory) success(`Synced into ${relativeDirectory} (${pluralize(effect.updatedCount, "file")} updated, ${effect.unchangedCount} unchanged)`);
|
|
2657
|
+
else success(`Synced ${pluralize(effect.totalCount, "file")} (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2620
2658
|
if (config.projectDirCreated && config.projectDir) tryGitInit(config.projectDir);
|
|
2621
2659
|
status("Watching for changes...");
|
|
2622
2660
|
return [];
|
|
@@ -2679,6 +2717,14 @@ async function start(config) {
|
|
|
2679
2717
|
}
|
|
2680
2718
|
(async () => {
|
|
2681
2719
|
cancelDisconnectMessage();
|
|
2720
|
+
if (syncState.mode !== "disconnected") {
|
|
2721
|
+
if (syncState.socket === client) {
|
|
2722
|
+
debug(`Ignoring duplicate handshake from active socket in ${syncState.mode} mode`);
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
debug(`New handshake received in ${syncState.mode} mode, resetting sync state`);
|
|
2726
|
+
await processEvent({ type: "DISCONNECT" });
|
|
2727
|
+
}
|
|
2682
2728
|
if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2683
2729
|
await processEvent({
|
|
2684
2730
|
type: "HANDSHAKE",
|
|
@@ -2709,7 +2755,7 @@ async function start(config) {
|
|
|
2709
2755
|
event = { type: "REQUEST_FILES" };
|
|
2710
2756
|
break;
|
|
2711
2757
|
case "file-list":
|
|
2712
|
-
debug(`Received file list: ${message.files.length}
|
|
2758
|
+
debug(`Received file list: ${pluralize(message.files.length, "file")}`);
|
|
2713
2759
|
event = {
|
|
2714
2760
|
type: "REMOTE_FILE_LIST",
|
|
2715
2761
|
files: message.files
|
|
@@ -2785,7 +2831,11 @@ async function start(config) {
|
|
|
2785
2831
|
}
|
|
2786
2832
|
})();
|
|
2787
2833
|
});
|
|
2788
|
-
connection.on("disconnect", () => {
|
|
2834
|
+
connection.on("disconnect", (client) => {
|
|
2835
|
+
if (syncState.socket !== client) {
|
|
2836
|
+
debug("[STATE] Ignoring disconnect from stale socket");
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2789
2839
|
scheduleDisconnectMessage(() => {
|
|
2790
2840
|
status("Disconnected, waiting to reconnect...");
|
|
2791
2841
|
});
|