framer-code-link 0.3.0 → 0.4.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/dist/index.mjs +196 -345
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "fs/promises";
|
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
6
6
|
import chokidar from "chokidar";
|
|
7
7
|
import path from "path";
|
|
8
|
+
import { getPortFromHash, isSupportedExtension, normalizePath, pluralize, sanitizeFilePath, shortProjectHash } from "@code-link/shared";
|
|
8
9
|
import { createHash } from "crypto";
|
|
9
10
|
import { setupTypeAcquisition } from "@typescript/ata";
|
|
10
11
|
import ts from "typescript";
|
|
@@ -118,7 +119,6 @@ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
|
|
|
118
119
|
let currentLevel = LogLevel.INFO;
|
|
119
120
|
let lastMessage = "";
|
|
120
121
|
let lastMessageCount = 0;
|
|
121
|
-
let lastCategory = "other";
|
|
122
122
|
const CLEAR_LINE = "\x1B[2K";
|
|
123
123
|
const MOVE_CURSOR_UP = "\x1B[1A";
|
|
124
124
|
function rewriteLastLine(text) {
|
|
@@ -144,16 +144,6 @@ function flushDedupe() {
|
|
|
144
144
|
lastMessageCount = 0;
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
* Handle category transition - adds newline when switching categories
|
|
148
|
-
*/
|
|
149
|
-
function transitionCategory(newCategory) {
|
|
150
|
-
if (lastCategory !== newCategory) {
|
|
151
|
-
flushDedupe();
|
|
152
|
-
console.log();
|
|
153
|
-
lastCategory = newCategory;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
147
|
* Log with deduplication - repeated messages within window get counted
|
|
158
148
|
*/
|
|
159
149
|
function logWithDedupe(message, writer) {
|
|
@@ -187,9 +177,10 @@ function debug(message, ...args) {
|
|
|
187
177
|
*/
|
|
188
178
|
function info(message, ...args) {
|
|
189
179
|
if (currentLevel <= LogLevel.INFO) {
|
|
190
|
-
transitionCategory("other");
|
|
191
180
|
const formatted = args.length > 0 ? `${message} ${args.join(" ")}` : message;
|
|
192
|
-
logWithDedupe(formatted, () =>
|
|
181
|
+
logWithDedupe(formatted, () => {
|
|
182
|
+
console.log(formatted);
|
|
183
|
+
});
|
|
193
184
|
}
|
|
194
185
|
}
|
|
195
186
|
/**
|
|
@@ -198,7 +189,6 @@ function info(message, ...args) {
|
|
|
198
189
|
function warn(message, ...args) {
|
|
199
190
|
if (currentLevel <= LogLevel.WARN) {
|
|
200
191
|
if (message === lastMessage) return;
|
|
201
|
-
transitionCategory("other");
|
|
202
192
|
flushDedupe();
|
|
203
193
|
lastMessage = message;
|
|
204
194
|
lastMessageCount = 1;
|
|
@@ -210,7 +200,6 @@ function warn(message, ...args) {
|
|
|
210
200
|
*/
|
|
211
201
|
function error(message, ...args) {
|
|
212
202
|
if (currentLevel <= LogLevel.ERROR) {
|
|
213
|
-
transitionCategory("other");
|
|
214
203
|
flushDedupe();
|
|
215
204
|
console.error(import_picocolors.default.red(`✗ ${message}`), ...args);
|
|
216
205
|
}
|
|
@@ -220,7 +209,6 @@ function error(message, ...args) {
|
|
|
220
209
|
*/
|
|
221
210
|
function success(message, ...args) {
|
|
222
211
|
if (currentLevel <= LogLevel.INFO) {
|
|
223
|
-
transitionCategory("other");
|
|
224
212
|
flushDedupe();
|
|
225
213
|
console.log(import_picocolors.default.green(`✓ ${message}`), ...args);
|
|
226
214
|
}
|
|
@@ -230,23 +218,26 @@ function success(message, ...args) {
|
|
|
230
218
|
*/
|
|
231
219
|
function fileDown(fileName) {
|
|
232
220
|
if (currentLevel <= LogLevel.INFO) {
|
|
233
|
-
transitionCategory("file-sync");
|
|
234
221
|
const msg = ` ${import_picocolors.default.blue("↓")} ${fileName}`;
|
|
235
|
-
logWithDedupe(msg, () =>
|
|
222
|
+
logWithDedupe(msg, () => {
|
|
223
|
+
console.log(msg);
|
|
224
|
+
});
|
|
236
225
|
}
|
|
237
226
|
}
|
|
238
227
|
function fileUp(fileName) {
|
|
239
228
|
if (currentLevel <= LogLevel.INFO) {
|
|
240
|
-
transitionCategory("file-sync");
|
|
241
229
|
const msg = ` ${import_picocolors.default.green("↑")} ${fileName}`;
|
|
242
|
-
logWithDedupe(msg, () =>
|
|
230
|
+
logWithDedupe(msg, () => {
|
|
231
|
+
console.log(msg);
|
|
232
|
+
});
|
|
243
233
|
}
|
|
244
234
|
}
|
|
245
235
|
function fileDelete(fileName) {
|
|
246
236
|
if (currentLevel <= LogLevel.INFO) {
|
|
247
|
-
transitionCategory("file-sync");
|
|
248
237
|
const msg = ` ${import_picocolors.default.red("×")} ${fileName}`;
|
|
249
|
-
logWithDedupe(msg, () =>
|
|
238
|
+
logWithDedupe(msg, () => {
|
|
239
|
+
console.log(msg);
|
|
240
|
+
});
|
|
250
241
|
}
|
|
251
242
|
}
|
|
252
243
|
/**
|
|
@@ -254,7 +245,6 @@ function fileDelete(fileName) {
|
|
|
254
245
|
*/
|
|
255
246
|
function status(message) {
|
|
256
247
|
if (currentLevel <= LogLevel.INFO) {
|
|
257
|
-
transitionCategory("other");
|
|
258
248
|
flushDedupe();
|
|
259
249
|
console.log(import_picocolors.default.dim(` ${message}`));
|
|
260
250
|
}
|
|
@@ -307,8 +297,7 @@ function resetDisconnectState() {
|
|
|
307
297
|
/**
|
|
308
298
|
* WebSocket connection helper
|
|
309
299
|
*
|
|
310
|
-
*
|
|
311
|
-
* simple callbacks. Keeps raw socket API localized.
|
|
300
|
+
* Wrapper around ws.Server that normalizes handshake and surfaces callbacks.
|
|
312
301
|
*/
|
|
313
302
|
/**
|
|
314
303
|
* Initializes a WebSocket server and returns a connection interface
|
|
@@ -360,7 +349,7 @@ function initConnection(port) {
|
|
|
360
349
|
}
|
|
361
350
|
});
|
|
362
351
|
ws.on("close", (code, reason) => {
|
|
363
|
-
debug(`Client disconnected (code: ${code}, reason: ${reason
|
|
352
|
+
debug(`Client disconnected (code: ${code}, reason: ${reason.toString()})`);
|
|
364
353
|
handlers.onDisconnect?.();
|
|
365
354
|
});
|
|
366
355
|
ws.on("error", (err) => {
|
|
@@ -369,10 +358,20 @@ function initConnection(port) {
|
|
|
369
358
|
});
|
|
370
359
|
resolve({
|
|
371
360
|
on(event, handler) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
361
|
+
switch (event) {
|
|
362
|
+
case "handshake":
|
|
363
|
+
handlers.onHandshake = handler;
|
|
364
|
+
break;
|
|
365
|
+
case "message":
|
|
366
|
+
handlers.onMessage = handler;
|
|
367
|
+
break;
|
|
368
|
+
case "disconnect":
|
|
369
|
+
handlers.onDisconnect = handler;
|
|
370
|
+
break;
|
|
371
|
+
case "error":
|
|
372
|
+
handlers.onError = handler;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
376
375
|
},
|
|
377
376
|
close() {
|
|
378
377
|
wss.close();
|
|
@@ -421,174 +420,7 @@ function sendMessage(socket, message) {
|
|
|
421
420
|
}
|
|
422
421
|
|
|
423
422
|
//#endregion
|
|
424
|
-
//#region
|
|
425
|
-
/**
|
|
426
|
-
* Base58 alphabet (no 0/O/I/l to avoid confusion)
|
|
427
|
-
*/
|
|
428
|
-
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
429
|
-
/**
|
|
430
|
-
* Derive a short, deterministic hash from the full Framer project hash.
|
|
431
|
-
* Uses a simple numeric hash encoded in base58 for compactness.
|
|
432
|
-
* Idempotent: if input is already the target length, returns it unchanged.
|
|
433
|
-
*/
|
|
434
|
-
function shortProjectHash(fullHash, length = 8) {
|
|
435
|
-
if (fullHash.length === length) return fullHash;
|
|
436
|
-
let h1 = 0;
|
|
437
|
-
let h2 = 0;
|
|
438
|
-
for (let i = 0; i < fullHash.length; i++) {
|
|
439
|
-
const char = fullHash.charCodeAt(i);
|
|
440
|
-
h1 = Math.imul(h1 ^ char, 2246822507);
|
|
441
|
-
h2 = Math.imul(h2 ^ char, 3266489909);
|
|
442
|
-
}
|
|
443
|
-
h1 ^= h2 >>> 16;
|
|
444
|
-
h2 ^= h1 >>> 13;
|
|
445
|
-
let result = "";
|
|
446
|
-
const combined = [Math.abs(h1), Math.abs(h2)];
|
|
447
|
-
for (const num of combined) {
|
|
448
|
-
let n = num >>> 0;
|
|
449
|
-
while (n > 0 && result.length < length) {
|
|
450
|
-
result += BASE58[n % 58];
|
|
451
|
-
n = Math.floor(n / 58);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
while (result.length < length) result += BASE58[0];
|
|
455
|
-
return result.slice(0, length);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
//#endregion
|
|
459
|
-
//#region ../shared/dist/ports.js
|
|
460
|
-
/**
|
|
461
|
-
* Generate a deterministic port number from a project hash (full or short).
|
|
462
|
-
* Port range: 3847-4096 (250 possible ports)
|
|
463
|
-
* Must match between CLI and plugin.
|
|
464
|
-
*
|
|
465
|
-
* Internally normalizes to the short id so both full and short inputs yield the same port.
|
|
466
|
-
*/
|
|
467
|
-
function getPortFromHash(projectHash) {
|
|
468
|
-
const shortId = shortProjectHash(projectHash);
|
|
469
|
-
let hash = 0;
|
|
470
|
-
for (let i = 0; i < shortId.length; i++) {
|
|
471
|
-
const char = shortId.charCodeAt(i);
|
|
472
|
-
hash = (hash << 5) - hash + char;
|
|
473
|
-
hash = hash & hash;
|
|
474
|
-
}
|
|
475
|
-
return 3847 + Math.abs(hash) % 250;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region ../shared/dist/paths.js
|
|
480
|
-
/**
|
|
481
|
-
* File path normalization utilities
|
|
482
|
-
* Framer code files include extensions in their paths (.tsx, .ts, etc.)
|
|
483
|
-
*/
|
|
484
|
-
const firstCharacterRegex = /^[a-zA-Z$_]/;
|
|
485
|
-
const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
|
|
486
|
-
const onlyDotsRegex = /^\.+$/;
|
|
487
|
-
const tsxExtension = ".tsx";
|
|
488
|
-
var NameType;
|
|
489
|
-
(function(NameType$1) {
|
|
490
|
-
NameType$1["Variable"] = "Variable";
|
|
491
|
-
NameType$1["Selector"] = "Selector";
|
|
492
|
-
NameType$1["Directory"] = "Directory";
|
|
493
|
-
})(NameType || (NameType = {}));
|
|
494
|
-
function sanitizedName(type, name) {
|
|
495
|
-
if (!name) return null;
|
|
496
|
-
let validName = name.trim();
|
|
497
|
-
if (validName.length === 0) return null;
|
|
498
|
-
const validFirstChar = type === NameType.Selector ? "_" : "$";
|
|
499
|
-
if (type === NameType.Directory) {
|
|
500
|
-
if (onlyDotsRegex.test(validName)) return null;
|
|
501
|
-
} else if (!firstCharacterRegex.test(validName)) validName = validFirstChar + validName;
|
|
502
|
-
validName = validName.replace(remainingCharactersRegex, "_");
|
|
503
|
-
validName = validName.replace(/_+/g, "_");
|
|
504
|
-
validName = validName.replace(/^\$_/u, validFirstChar);
|
|
505
|
-
return validName;
|
|
506
|
-
}
|
|
507
|
-
function sanitizedVariableName(name) {
|
|
508
|
-
return sanitizedName(NameType.Variable, name);
|
|
509
|
-
}
|
|
510
|
-
function sanitizedDirectoryName(name) {
|
|
511
|
-
return sanitizedName(NameType.Directory, name);
|
|
512
|
-
}
|
|
513
|
-
function capitalizeFirstLetter(str) {
|
|
514
|
-
if (str.length === 0) return str;
|
|
515
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
516
|
-
}
|
|
517
|
-
function hasValidExtension(fileName) {
|
|
518
|
-
if (fileName.endsWith(".json")) return true;
|
|
519
|
-
return /\.[tj]sx?$/u.test(fileName);
|
|
520
|
-
}
|
|
521
|
-
function splitExtension(fileName) {
|
|
522
|
-
const match = fileName.match(/^(.+?)(\.[^.]+)?$/);
|
|
523
|
-
if (!match) return [fileName, ""];
|
|
524
|
-
return [match[1], match[2]?.slice(1) || ""];
|
|
525
|
-
}
|
|
526
|
-
function dirname(filePath) {
|
|
527
|
-
const at = filePath.lastIndexOf("/");
|
|
528
|
-
if (at < 0) return "";
|
|
529
|
-
return filePath.slice(0, at);
|
|
530
|
-
}
|
|
531
|
-
function filename(filePath) {
|
|
532
|
-
const at = filePath.lastIndexOf("/") + 1;
|
|
533
|
-
return filePath.slice(at);
|
|
534
|
-
}
|
|
535
|
-
function pathJoin(...parts) {
|
|
536
|
-
let res = "";
|
|
537
|
-
parts.forEach((part) => {
|
|
538
|
-
while (part.startsWith("/")) part = part.slice(1);
|
|
539
|
-
while (part.endsWith("/")) part = part.slice(0, -1);
|
|
540
|
-
if (part === "") return;
|
|
541
|
-
if (res !== "") res += "/";
|
|
542
|
-
res += part;
|
|
543
|
-
});
|
|
544
|
-
return res;
|
|
545
|
-
}
|
|
546
|
-
function normalizePath(filePath) {
|
|
547
|
-
if (!filePath) return "";
|
|
548
|
-
const isAbsolute = filePath.startsWith("/");
|
|
549
|
-
const segments = filePath.replace(/\\/g, "/").split("/");
|
|
550
|
-
const stack = [];
|
|
551
|
-
for (const segment of segments) {
|
|
552
|
-
if (!segment || segment === ".") continue;
|
|
553
|
-
if (segment === "..") {
|
|
554
|
-
if (stack.length > 0) stack.pop();
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
stack.push(segment);
|
|
558
|
-
}
|
|
559
|
-
const normalized = stack.join("/");
|
|
560
|
-
if (isAbsolute) return `/${normalized}`;
|
|
561
|
-
return normalized;
|
|
562
|
-
}
|
|
563
|
-
function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
564
|
-
const trimmed = input.trim();
|
|
565
|
-
let [inputName, extension] = splitExtension(filename(trimmed));
|
|
566
|
-
if (extension) extension = `.${extension}`;
|
|
567
|
-
const dirName = dirname(trimmed).split("/").map((part) => sanitizedDirectoryName(part)).filter((part) => Boolean(part)).join("/");
|
|
568
|
-
let name = sanitizedVariableName(inputName) ?? "MyComponent";
|
|
569
|
-
if ((!hasValidExtension(extension) || extension === tsxExtension) && capitalizeReactComponent) name = capitalizeFirstLetter(name);
|
|
570
|
-
return {
|
|
571
|
-
path: pathJoin(dirName, name + extension),
|
|
572
|
-
dirName,
|
|
573
|
-
name,
|
|
574
|
-
extension
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
function isSupportedExtension$1(filePath) {
|
|
578
|
-
return /\.(tsx?|jsx?|json)$/i.test(filePath);
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Pluralize a word based on count
|
|
582
|
-
* @example pluralize(1, "file") => "1 file"
|
|
583
|
-
* @example pluralize(3, "file") => "3 files"
|
|
584
|
-
* @example pluralize(0, "conflict") => "0 conflicts"
|
|
585
|
-
*/
|
|
586
|
-
function pluralize(count, singular, plural) {
|
|
587
|
-
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
//#endregion
|
|
591
|
-
//#region src/utils/paths.ts
|
|
423
|
+
//#region src/utils/node-paths.ts
|
|
592
424
|
/**
|
|
593
425
|
* Path manipulation utilities
|
|
594
426
|
*/
|
|
@@ -627,9 +459,7 @@ function normalizePath$1(filePath) {
|
|
|
627
459
|
/**
|
|
628
460
|
* File watcher helper
|
|
629
461
|
*
|
|
630
|
-
*
|
|
631
|
-
* only supported file types (ts, tsx, js, json). Controller never worries
|
|
632
|
-
* about addDir or platform separators.
|
|
462
|
+
* Wrapper around chokidar that normalizes file paths and filters to ts, tsx, js, json.
|
|
633
463
|
*/
|
|
634
464
|
/**
|
|
635
465
|
* Initializes a file watcher for the given directory
|
|
@@ -637,13 +467,13 @@ function normalizePath$1(filePath) {
|
|
|
637
467
|
function initWatcher(filesDir) {
|
|
638
468
|
const handlers = [];
|
|
639
469
|
const watcher = chokidar.watch(filesDir, {
|
|
640
|
-
ignored: /(^|[
|
|
470
|
+
ignored: /(^|[/\\])\.\./,
|
|
641
471
|
persistent: true,
|
|
642
472
|
ignoreInitial: false
|
|
643
473
|
});
|
|
644
474
|
debug(`Watching directory: ${filesDir}`);
|
|
645
475
|
const emitEvent = async (kind, absolutePath) => {
|
|
646
|
-
if (!isSupportedExtension
|
|
476
|
+
if (!isSupportedExtension(absolutePath)) return;
|
|
647
477
|
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
648
478
|
const relativePath = sanitizeFilePath(rawRelativePath, false).path;
|
|
649
479
|
let effectiveAbsolutePath = absolutePath;
|
|
@@ -673,12 +503,18 @@ function initWatcher(filesDir) {
|
|
|
673
503
|
debug(`Watcher event: ${kind} ${relativePath}`);
|
|
674
504
|
for (const handler of handlers) handler(event);
|
|
675
505
|
};
|
|
676
|
-
watcher.on("add", (filePath) =>
|
|
677
|
-
|
|
678
|
-
|
|
506
|
+
watcher.on("add", (filePath) => {
|
|
507
|
+
emitEvent("add", filePath);
|
|
508
|
+
});
|
|
509
|
+
watcher.on("change", (filePath) => {
|
|
510
|
+
emitEvent("change", filePath);
|
|
511
|
+
});
|
|
512
|
+
watcher.on("unlink", (filePath) => {
|
|
513
|
+
emitEvent("delete", filePath);
|
|
514
|
+
});
|
|
679
515
|
return {
|
|
680
|
-
on(
|
|
681
|
-
|
|
516
|
+
on(_event, handler) {
|
|
517
|
+
handlers.push(handler);
|
|
682
518
|
},
|
|
683
519
|
async close() {
|
|
684
520
|
await watcher.close();
|
|
@@ -696,7 +532,7 @@ function initWatcher(filesDir) {
|
|
|
696
532
|
* (hash matches), because that means the file wasn't edited while CLI was offline.
|
|
697
533
|
*/
|
|
698
534
|
const STATE_FILE_NAME = ".framer-sync-state.json";
|
|
699
|
-
const CURRENT_VERSION =
|
|
535
|
+
const CURRENT_VERSION = 1;
|
|
700
536
|
const SUPPORTED_EXTENSIONS$1 = [
|
|
701
537
|
".ts",
|
|
702
538
|
".tsx",
|
|
@@ -801,7 +637,7 @@ async function listFiles(filesDir) {
|
|
|
801
637
|
await walk(entryPath);
|
|
802
638
|
continue;
|
|
803
639
|
}
|
|
804
|
-
if (!isSupportedExtension(entry.name)) continue;
|
|
640
|
+
if (!isSupportedExtension$1(entry.name)) continue;
|
|
805
641
|
const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
|
|
806
642
|
try {
|
|
807
643
|
const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
|
|
@@ -831,7 +667,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
831
667
|
const preferRemote = options.preferRemote ?? false;
|
|
832
668
|
const persistedState = options.persistedState;
|
|
833
669
|
const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
|
|
834
|
-
debug(`Detecting conflicts for ${remoteFiles.length} remote files`);
|
|
670
|
+
debug(`Detecting conflicts for ${String(remoteFiles.length)} remote files`);
|
|
835
671
|
const localFiles = await listFiles(filesDir);
|
|
836
672
|
const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
|
|
837
673
|
const remoteFileMap = new Map(remoteFiles.map((f) => {
|
|
@@ -854,7 +690,7 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
854
690
|
localContent: null,
|
|
855
691
|
remoteContent: remote.content,
|
|
856
692
|
remoteModifiedAt: remote.modifiedAt,
|
|
857
|
-
lastSyncedAt: persisted
|
|
693
|
+
lastSyncedAt: persisted.timestamp
|
|
858
694
|
});
|
|
859
695
|
} else writes.push({
|
|
860
696
|
name: normalized.relativePath,
|
|
@@ -896,13 +732,13 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
896
732
|
const persisted = getPersistedState(local.name);
|
|
897
733
|
if (persisted) {
|
|
898
734
|
const localClean = hashFileContent(local.content) === persisted.contentHash;
|
|
899
|
-
debug(`Conflict: ${local.name} deleted in Framer (localClean=${localClean})`);
|
|
735
|
+
debug(`Conflict: ${local.name} deleted in Framer (localClean=${String(localClean)})`);
|
|
900
736
|
conflicts.push({
|
|
901
737
|
fileName: local.name,
|
|
902
738
|
localContent: local.content,
|
|
903
739
|
remoteContent: null,
|
|
904
740
|
localModifiedAt: local.modifiedAt,
|
|
905
|
-
lastSyncedAt: persisted
|
|
741
|
+
lastSyncedAt: persisted.timestamp,
|
|
906
742
|
localClean
|
|
907
743
|
});
|
|
908
744
|
} else localOnly.push({
|
|
@@ -1006,7 +842,7 @@ async function deleteLocalFile(fileName, filesDir, hashTracker) {
|
|
|
1006
842
|
hashTracker.forget(normalized.relativePath);
|
|
1007
843
|
debug(`Deleted file: ${normalized.relativePath}`);
|
|
1008
844
|
} catch (err) {
|
|
1009
|
-
if (err
|
|
845
|
+
if (err.code === "ENOENT") {
|
|
1010
846
|
hashTracker.forget(normalized.relativePath);
|
|
1011
847
|
debug(`File already deleted: ${normalized.relativePath}`);
|
|
1012
848
|
return;
|
|
@@ -1026,6 +862,15 @@ async function readFileSafe(fileName, filesDir) {
|
|
|
1026
862
|
return null;
|
|
1027
863
|
}
|
|
1028
864
|
}
|
|
865
|
+
/**
|
|
866
|
+
* Filter out files whose content matches the last remembered hash.
|
|
867
|
+
* Used to skip inbound echoes of our own local sends.
|
|
868
|
+
*/
|
|
869
|
+
function filterEchoedFiles(files, hashTracker) {
|
|
870
|
+
return files.filter((file) => {
|
|
871
|
+
return !hashTracker.shouldSkip(file.name, file.content);
|
|
872
|
+
});
|
|
873
|
+
}
|
|
1029
874
|
function resolveRemoteReference(filesDir, rawName) {
|
|
1030
875
|
const normalized = sanitizeRelativePath(rawName);
|
|
1031
876
|
const absolutePath = path.join(filesDir, normalized.relativePath);
|
|
@@ -1043,7 +888,7 @@ function sanitizeRelativePath(relativePath) {
|
|
|
1043
888
|
extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
|
|
1044
889
|
};
|
|
1045
890
|
}
|
|
1046
|
-
function isSupportedExtension(fileName) {
|
|
891
|
+
function isSupportedExtension$1(fileName) {
|
|
1047
892
|
const lower = fileName.toLowerCase();
|
|
1048
893
|
return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
1049
894
|
}
|
|
@@ -1056,7 +901,7 @@ function isSupportedExtension(fileName) {
|
|
|
1056
901
|
function extractImports(code) {
|
|
1057
902
|
const imports = [];
|
|
1058
903
|
const seen = /* @__PURE__ */ new Set();
|
|
1059
|
-
const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([
|
|
904
|
+
const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([^./][^'"]+)['"]/g;
|
|
1060
905
|
const urlRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]https?:\/\/[^'"]+['"]/g;
|
|
1061
906
|
let match;
|
|
1062
907
|
while ((match = npmRegex.exec(code)) !== null) {
|
|
@@ -1088,7 +933,7 @@ function extractImports(code) {
|
|
|
1088
933
|
* Attempt to derive an npm-style package specifier from a URL import.
|
|
1089
934
|
*/
|
|
1090
935
|
function extractPackageFromUrl(url) {
|
|
1091
|
-
return
|
|
936
|
+
return /\/(@?[^@/]+(?:\/[^@/]+)?)/.exec(url)?.[1] ?? null;
|
|
1092
937
|
}
|
|
1093
938
|
|
|
1094
939
|
//#endregion
|
|
@@ -1141,29 +986,31 @@ var Installer = class {
|
|
|
1141
986
|
},
|
|
1142
987
|
progress: () => {},
|
|
1143
988
|
finished: (files) => {
|
|
1144
|
-
if (files
|
|
989
|
+
if (files.size > 0) debug("ATA: type acquisition complete");
|
|
1145
990
|
},
|
|
1146
991
|
errorMessage: (message, error$1) => {
|
|
1147
992
|
warn(`ATA warning: ${message}`, error$1);
|
|
1148
993
|
},
|
|
1149
|
-
receivedFile:
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
if (
|
|
1156
|
-
seenPackages.
|
|
1157
|
-
|
|
994
|
+
receivedFile: (code, receivedPath) => {
|
|
995
|
+
(async () => {
|
|
996
|
+
const normalized = receivedPath.replace(/^\//, "");
|
|
997
|
+
const destination = path.join(this.projectDir, normalized);
|
|
998
|
+
const pkgMatch = /\/node_modules\/(@?[^/]+(?:\/[^/]+)?)\//.exec(receivedPath);
|
|
999
|
+
try {
|
|
1000
|
+
if (await fs.readFile(destination, "utf-8") === code) {
|
|
1001
|
+
if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
|
|
1002
|
+
seenPackages.add(pkgMatch[1]);
|
|
1003
|
+
debug(`📦 Types: ${pkgMatch[1]} (from disk cache)`);
|
|
1004
|
+
}
|
|
1005
|
+
return;
|
|
1158
1006
|
}
|
|
1159
|
-
|
|
1007
|
+
} catch {}
|
|
1008
|
+
if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
|
|
1009
|
+
seenPackages.add(pkgMatch[1]);
|
|
1010
|
+
debug(`📦 Types: ${pkgMatch[1]}`);
|
|
1160
1011
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
seenPackages.add(pkgMatch[1]);
|
|
1164
|
-
debug(`📦 Types: ${pkgMatch[1]}`);
|
|
1165
|
-
}
|
|
1166
|
-
await this.writeTypeFile(receivedPath, code);
|
|
1012
|
+
await this.writeTypeFile(receivedPath, code);
|
|
1013
|
+
})();
|
|
1167
1014
|
}
|
|
1168
1015
|
}
|
|
1169
1016
|
});
|
|
@@ -1210,10 +1057,10 @@ var Installer = class {
|
|
|
1210
1057
|
});
|
|
1211
1058
|
}
|
|
1212
1059
|
async processImports(fileName, content) {
|
|
1213
|
-
const allImports = extractImports(content).filter((
|
|
1060
|
+
const allImports = extractImports(content).filter((i) => i.type === "npm");
|
|
1214
1061
|
if (allImports.length === 0) return;
|
|
1215
|
-
const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((
|
|
1216
|
-
if (allImports.length - imports.length > 0 && !this.allowUnsupportedNpm) debug(`Skipping unsupported packages: ${allImports.filter((
|
|
1062
|
+
const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((i) => this.isSupportedPackage(i.name));
|
|
1063
|
+
if (allImports.length - imports.length > 0 && !this.allowUnsupportedNpm) debug(`Skipping unsupported packages: ${allImports.filter((i) => !this.isSupportedPackage(i.name)).map((i) => i.name).join(", ")} (use --unsupported-npm to enable)`);
|
|
1217
1064
|
if (imports.length === 0) return;
|
|
1218
1065
|
const hash = imports.map((imp) => imp.name).sort().join(",");
|
|
1219
1066
|
if (this.processedImports.has(hash)) return;
|
|
@@ -1251,11 +1098,11 @@ var Installer = class {
|
|
|
1251
1098
|
warn(`Failed to write type file ${destination}`, err);
|
|
1252
1099
|
return;
|
|
1253
1100
|
}
|
|
1254
|
-
if (
|
|
1101
|
+
if (/node_modules\/@types\/[^/]+\/index\.d\.ts$/.exec(normalized)) await this.ensureTypesPackageJson(normalized);
|
|
1255
1102
|
if (normalized.includes("node_modules/@types/react/index.d.ts")) await this.patchReactTypes(destination);
|
|
1256
1103
|
}
|
|
1257
1104
|
async ensureTypesPackageJson(normalizedPath) {
|
|
1258
|
-
const pkgMatch =
|
|
1105
|
+
const pkgMatch = /node_modules\/(@types\/[^/]+)\//.exec(normalizedPath);
|
|
1259
1106
|
if (!pkgMatch) return;
|
|
1260
1107
|
const pkgName = pkgMatch[1];
|
|
1261
1108
|
const pkgDir = path.join(this.projectDir, "node_modules", pkgName);
|
|
@@ -1267,16 +1114,7 @@ var Installer = class {
|
|
|
1267
1114
|
const version$1 = npmData["dist-tags"]?.latest;
|
|
1268
1115
|
if (!version$1 || !npmData.versions?.[version$1]) return;
|
|
1269
1116
|
const pkg = npmData.versions[version$1];
|
|
1270
|
-
if (pkg.exports
|
|
1271
|
-
const fixExport = (value) => {
|
|
1272
|
-
if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
|
|
1273
|
-
if (value && typeof value === "object") {
|
|
1274
|
-
if ((value.import || value.require) && !value.types) value.types = (value.import || value.require).replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
|
|
1275
|
-
}
|
|
1276
|
-
return value;
|
|
1277
|
-
};
|
|
1278
|
-
for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExport(pkg.exports[key]);
|
|
1279
|
-
}
|
|
1117
|
+
if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
|
|
1280
1118
|
await fs.mkdir(pkgDir, { recursive: true });
|
|
1281
1119
|
await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
1282
1120
|
} catch {}
|
|
@@ -1286,7 +1124,7 @@ var Installer = class {
|
|
|
1286
1124
|
let content = await fs.readFile(destination, "utf-8");
|
|
1287
1125
|
if (content.includes("function useRef<T = undefined>()")) return;
|
|
1288
1126
|
const overloadPattern = /function useRef<T>\(initialValue: T \| undefined\): RefObject<T \| undefined>;/;
|
|
1289
|
-
if (!
|
|
1127
|
+
if (!content.includes("function useRef<T>(initialValue: T | undefined)")) return;
|
|
1290
1128
|
content = content.replace(overloadPattern, `function useRef<T>(initialValue: T | undefined): RefObject<T | undefined>;
|
|
1291
1129
|
function useRef<T = undefined>(): MutableRefObject<T | undefined>;`);
|
|
1292
1130
|
await fs.writeFile(destination, content, "utf-8");
|
|
@@ -1426,11 +1264,24 @@ declare module "*.json"
|
|
|
1426
1264
|
}));
|
|
1427
1265
|
}
|
|
1428
1266
|
};
|
|
1267
|
+
/**
|
|
1268
|
+
* Transform package.json exports to include .d.ts type paths
|
|
1269
|
+
*/
|
|
1270
|
+
function fixExportTypes(value) {
|
|
1271
|
+
if (typeof value === "string") return { types: value.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts") };
|
|
1272
|
+
if ((value.import ?? value.require) && !value.types) value.types = (value.import ?? value.require)?.replace(/\.js$/, ".d.ts").replace(/\.cjs$/, ".d.cts");
|
|
1273
|
+
return value;
|
|
1274
|
+
}
|
|
1429
1275
|
async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
|
|
1430
|
-
|
|
1276
|
+
let urlString;
|
|
1277
|
+
if (typeof url === "string") urlString = url;
|
|
1278
|
+
else if (url instanceof URL) urlString = url.href;
|
|
1279
|
+
else urlString = url.url;
|
|
1431
1280
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1432
1281
|
const controller = new AbortController();
|
|
1433
|
-
const timeout = setTimeout(() =>
|
|
1282
|
+
const timeout = setTimeout(() => {
|
|
1283
|
+
controller.abort();
|
|
1284
|
+
}, FETCH_TIMEOUT_MS);
|
|
1434
1285
|
try {
|
|
1435
1286
|
const response = await fetch(url, {
|
|
1436
1287
|
...init,
|
|
@@ -1438,12 +1289,13 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
|
|
|
1438
1289
|
});
|
|
1439
1290
|
clearTimeout(timeout);
|
|
1440
1291
|
return response;
|
|
1441
|
-
} catch (
|
|
1292
|
+
} catch (err) {
|
|
1442
1293
|
clearTimeout(timeout);
|
|
1443
|
-
const
|
|
1294
|
+
const error$1 = err;
|
|
1295
|
+
const isRetryable = error$1.cause?.code === "ECONNRESET" || error$1.cause?.code === "ETIMEDOUT" || error$1.cause?.code === "UND_ERR_CONNECT_TIMEOUT" || error$1.message.includes("timeout");
|
|
1444
1296
|
if (attempt < retries && isRetryable) {
|
|
1445
1297
|
const delay = attempt * 1e3;
|
|
1446
|
-
warn(`Fetch failed (${error$1
|
|
1298
|
+
warn(`Fetch failed (${error$1.cause?.code ?? error$1.message}) for ${urlString}, retrying in ${delay}ms...`);
|
|
1447
1299
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1448
1300
|
continue;
|
|
1449
1301
|
}
|
|
@@ -1575,11 +1427,12 @@ var FileMetadataCache = class {
|
|
|
1575
1427
|
if (this.pendingPersist) await this.pendingPersist;
|
|
1576
1428
|
}
|
|
1577
1429
|
schedulePersist() {
|
|
1578
|
-
|
|
1579
|
-
if (!
|
|
1430
|
+
const projectDir = this.projectDir;
|
|
1431
|
+
if (!projectDir) return;
|
|
1432
|
+
this.pendingPersist ??= (async () => {
|
|
1580
1433
|
try {
|
|
1581
1434
|
await Promise.resolve();
|
|
1582
|
-
await savePersistedState(
|
|
1435
|
+
await savePersistedState(projectDir, this.persisted);
|
|
1583
1436
|
} finally {
|
|
1584
1437
|
this.pendingPersist = null;
|
|
1585
1438
|
}
|
|
@@ -1598,12 +1451,24 @@ var PluginDisconnectedError = class extends Error {
|
|
|
1598
1451
|
var UserActionCoordinator = class {
|
|
1599
1452
|
pendingActions = /* @__PURE__ */ new Map();
|
|
1600
1453
|
/**
|
|
1454
|
+
* Register a pending action and return a typed promise
|
|
1455
|
+
*/
|
|
1456
|
+
awaitAction(actionId, description) {
|
|
1457
|
+
return new Promise((resolve, reject) => {
|
|
1458
|
+
this.pendingActions.set(actionId, {
|
|
1459
|
+
resolve,
|
|
1460
|
+
reject
|
|
1461
|
+
});
|
|
1462
|
+
debug(`Awaiting ${description}: ${actionId}`);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1601
1466
|
* Sends the delete request to the plugin and awaits the user's decision
|
|
1602
1467
|
*/
|
|
1603
1468
|
async requestDeleteDecision(socket, { fileName, requireConfirmation }) {
|
|
1604
1469
|
if (!socket) throw new Error("Cannot request delete decision: plugin not connected");
|
|
1605
1470
|
if (requireConfirmation) {
|
|
1606
|
-
const confirmationPromise = this.
|
|
1471
|
+
const confirmationPromise = this.awaitAction(`delete:${fileName}`, "delete confirmation");
|
|
1607
1472
|
await sendMessage(socket, {
|
|
1608
1473
|
type: "file-delete",
|
|
1609
1474
|
fileNames: [fileName],
|
|
@@ -1634,7 +1499,7 @@ var UserActionCoordinator = class {
|
|
|
1634
1499
|
if (conflicts.length === 0) return /* @__PURE__ */ new Map();
|
|
1635
1500
|
const pending = conflicts.map((conflict) => ({
|
|
1636
1501
|
fileName: conflict.fileName,
|
|
1637
|
-
promise: this.
|
|
1502
|
+
promise: this.awaitAction(`conflict:${conflict.fileName}`, "conflict resolution")
|
|
1638
1503
|
}));
|
|
1639
1504
|
await sendMessage(socket, {
|
|
1640
1505
|
type: "conflicts-detected",
|
|
@@ -1652,18 +1517,6 @@ var UserActionCoordinator = class {
|
|
|
1652
1517
|
}
|
|
1653
1518
|
}
|
|
1654
1519
|
/**
|
|
1655
|
-
* Generic confirmation awaiter
|
|
1656
|
-
*/
|
|
1657
|
-
awaitConfirmation(actionId, description) {
|
|
1658
|
-
return new Promise((resolve, reject) => {
|
|
1659
|
-
this.pendingActions.set(actionId, {
|
|
1660
|
-
resolve,
|
|
1661
|
-
reject
|
|
1662
|
-
});
|
|
1663
|
-
debug(`Awaiting ${description}: ${actionId}`);
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
/**
|
|
1667
1520
|
* Handle incoming confirmation response
|
|
1668
1521
|
*/
|
|
1669
1522
|
handleConfirmation(actionId, value) {
|
|
@@ -1700,7 +1553,7 @@ var UserActionCoordinator = class {
|
|
|
1700
1553
|
* Note: This is for INCOMING changes from remote. Local changes (from watcher)
|
|
1701
1554
|
* are handled separately and always sent during watching mode.
|
|
1702
1555
|
*/
|
|
1703
|
-
function validateIncomingChange(
|
|
1556
|
+
function validateIncomingChange(fileMeta, currentMode) {
|
|
1704
1557
|
if (currentMode === "snapshot_processing" || currentMode === "handshaking") return {
|
|
1705
1558
|
action: "queue",
|
|
1706
1559
|
reason: "snapshot-in-progress"
|
|
@@ -1751,7 +1604,7 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1751
1604
|
const cwd = process.cwd();
|
|
1752
1605
|
const existing = await findExistingProjectDir(cwd, projectHash);
|
|
1753
1606
|
if (existing) return existing;
|
|
1754
|
-
if (!projectName) throw new Error("
|
|
1607
|
+
if (!projectName) throw new Error("Failed to get Project name. Pass --name <project name>.");
|
|
1755
1608
|
const dirName = toDirName(projectName);
|
|
1756
1609
|
const pkgName = toPackageName(projectName);
|
|
1757
1610
|
const shortId = shortProjectHash(projectHash);
|
|
@@ -1792,9 +1645,10 @@ async function matchesProject(packageJsonPath, projectHash) {
|
|
|
1792
1645
|
//#endregion
|
|
1793
1646
|
//#region src/controller.ts
|
|
1794
1647
|
/**
|
|
1795
|
-
* Controller
|
|
1796
|
-
*
|
|
1797
|
-
*
|
|
1648
|
+
* CLI Controller
|
|
1649
|
+
*
|
|
1650
|
+
* All runtime state and orchestrates the sync lifecycle.
|
|
1651
|
+
* Helpers should provide data, nevering hold control or callbacks.
|
|
1798
1652
|
*/
|
|
1799
1653
|
/** Log helper */
|
|
1800
1654
|
function log(level, message) {
|
|
@@ -1805,16 +1659,6 @@ function log(level, message) {
|
|
|
1805
1659
|
};
|
|
1806
1660
|
}
|
|
1807
1661
|
/**
|
|
1808
|
-
* Filter out files whose content matches the last remembered hash.
|
|
1809
|
-
* Used to skip inbound echoes of our own local sends.
|
|
1810
|
-
*/
|
|
1811
|
-
function filterEchoedFiles(files, hashTracker) {
|
|
1812
|
-
return files.filter((file) => {
|
|
1813
|
-
if (file.content === void 0) return true;
|
|
1814
|
-
return !hashTracker.shouldSkip(file.name, file.content);
|
|
1815
|
-
});
|
|
1816
|
-
}
|
|
1817
|
-
/**
|
|
1818
1662
|
* Pure state transition function
|
|
1819
1663
|
* Takes current state + event, returns new state + effects to execute
|
|
1820
1664
|
*/
|
|
@@ -1844,7 +1688,7 @@ function transition(state, event) {
|
|
|
1844
1688
|
},
|
|
1845
1689
|
effects
|
|
1846
1690
|
};
|
|
1847
|
-
case "
|
|
1691
|
+
case "FILE_SYNCED_CONFIRMATION":
|
|
1848
1692
|
effects.push(log("debug", `Remote confirmed sync: ${event.fileName}`), {
|
|
1849
1693
|
type: "UPDATE_FILE_METADATA",
|
|
1850
1694
|
fileName: event.fileName,
|
|
@@ -1905,7 +1749,7 @@ function transition(state, event) {
|
|
|
1905
1749
|
state: {
|
|
1906
1750
|
...state,
|
|
1907
1751
|
mode: "snapshot_processing",
|
|
1908
|
-
|
|
1752
|
+
pendingRemoteChanges: event.files
|
|
1909
1753
|
},
|
|
1910
1754
|
effects
|
|
1911
1755
|
};
|
|
@@ -1948,7 +1792,7 @@ function transition(state, event) {
|
|
|
1948
1792
|
effects
|
|
1949
1793
|
};
|
|
1950
1794
|
}
|
|
1951
|
-
const remoteTotal = state.
|
|
1795
|
+
const remoteTotal = state.pendingRemoteChanges.length;
|
|
1952
1796
|
const totalCount = remoteTotal + localOnly.length;
|
|
1953
1797
|
const updatedCount = safeWrites.length + localOnly.length;
|
|
1954
1798
|
const unchangedCount = Math.max(0, remoteTotal - safeWrites.length);
|
|
@@ -1962,19 +1806,19 @@ function transition(state, event) {
|
|
|
1962
1806
|
state: {
|
|
1963
1807
|
...state,
|
|
1964
1808
|
mode: "watching",
|
|
1965
|
-
|
|
1809
|
+
pendingRemoteChanges: []
|
|
1966
1810
|
},
|
|
1967
1811
|
effects
|
|
1968
1812
|
};
|
|
1969
1813
|
}
|
|
1970
1814
|
case "FILE_CHANGE": {
|
|
1971
|
-
const validation = validateIncomingChange(event.
|
|
1815
|
+
const validation = validateIncomingChange(event.fileMeta, state.mode);
|
|
1972
1816
|
if (validation.action === "queue") {
|
|
1973
1817
|
effects.push(log("debug", `Queueing file change: ${event.file.name} (${validation.reason})`));
|
|
1974
1818
|
return {
|
|
1975
1819
|
state: {
|
|
1976
1820
|
...state,
|
|
1977
|
-
|
|
1821
|
+
pendingRemoteChanges: [...state.pendingRemoteChanges, event.file]
|
|
1978
1822
|
},
|
|
1979
1823
|
effects
|
|
1980
1824
|
};
|
|
@@ -2012,7 +1856,7 @@ function transition(state, event) {
|
|
|
2012
1856
|
state,
|
|
2013
1857
|
effects
|
|
2014
1858
|
};
|
|
2015
|
-
case "
|
|
1859
|
+
case "LOCAL_DELETE_APPROVED":
|
|
2016
1860
|
effects.push(log("debug", `Delete confirmed: ${event.fileName}`), {
|
|
2017
1861
|
type: "DELETE_LOCAL_FILES",
|
|
2018
1862
|
names: [event.fileName]
|
|
@@ -2021,7 +1865,7 @@ function transition(state, event) {
|
|
|
2021
1865
|
state,
|
|
2022
1866
|
effects
|
|
2023
1867
|
};
|
|
2024
|
-
case "
|
|
1868
|
+
case "LOCAL_DELETE_REJECTED":
|
|
2025
1869
|
effects.push(log("debug", `Delete cancelled: ${event.fileName}`));
|
|
2026
1870
|
effects.push({
|
|
2027
1871
|
type: "WRITE_FILES",
|
|
@@ -2194,7 +2038,7 @@ function transition(state, event) {
|
|
|
2194
2038
|
state: {
|
|
2195
2039
|
...rest,
|
|
2196
2040
|
mode: "watching",
|
|
2197
|
-
|
|
2041
|
+
pendingRemoteChanges: []
|
|
2198
2042
|
},
|
|
2199
2043
|
effects
|
|
2200
2044
|
};
|
|
@@ -2376,14 +2220,16 @@ async function executeEffect(effect, context) {
|
|
|
2376
2220
|
status("Watching for changes...");
|
|
2377
2221
|
return [];
|
|
2378
2222
|
}
|
|
2379
|
-
case "LOG":
|
|
2380
|
-
|
|
2223
|
+
case "LOG": {
|
|
2224
|
+
const logFn = {
|
|
2381
2225
|
info,
|
|
2382
2226
|
warn,
|
|
2383
2227
|
success,
|
|
2384
2228
|
debug
|
|
2385
|
-
}[effect.level]
|
|
2229
|
+
}[effect.level];
|
|
2230
|
+
logFn(effect.message);
|
|
2386
2231
|
return [];
|
|
2232
|
+
}
|
|
2387
2233
|
}
|
|
2388
2234
|
}
|
|
2389
2235
|
/**
|
|
@@ -2397,7 +2243,7 @@ async function start(config) {
|
|
|
2397
2243
|
let syncState = {
|
|
2398
2244
|
mode: "disconnected",
|
|
2399
2245
|
socket: null,
|
|
2400
|
-
|
|
2246
|
+
pendingRemoteChanges: [],
|
|
2401
2247
|
pendingOperations: /* @__PURE__ */ new Map(),
|
|
2402
2248
|
nextOperationId: 1
|
|
2403
2249
|
};
|
|
@@ -2423,7 +2269,7 @@ async function start(config) {
|
|
|
2423
2269
|
}
|
|
2424
2270
|
}
|
|
2425
2271
|
const connection = await initConnection(config.port);
|
|
2426
|
-
connection.on("handshake",
|
|
2272
|
+
connection.on("handshake", (client, message) => {
|
|
2427
2273
|
debug(`Received handshake: ${message.projectName} (${message.projectId})`);
|
|
2428
2274
|
const expectedShort = shortProjectHash(config.projectHash);
|
|
2429
2275
|
const receivedShort = shortProjectHash(message.projectId);
|
|
@@ -2432,24 +2278,26 @@ async function start(config) {
|
|
|
2432
2278
|
client.close();
|
|
2433
2279
|
return;
|
|
2434
2280
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
if (config.projectDir && !installer) {
|
|
2444
|
-
installer = new Installer({
|
|
2445
|
-
projectDir: config.projectDir,
|
|
2446
|
-
allowUnsupportedNpm: config.allowUnsupportedNpm
|
|
2281
|
+
(async () => {
|
|
2282
|
+
await processEvent({
|
|
2283
|
+
type: "HANDSHAKE",
|
|
2284
|
+
socket: client,
|
|
2285
|
+
projectInfo: {
|
|
2286
|
+
projectId: message.projectId,
|
|
2287
|
+
projectName: message.projectName
|
|
2288
|
+
}
|
|
2447
2289
|
});
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2290
|
+
if (config.projectDir && !installer) {
|
|
2291
|
+
installer = new Installer({
|
|
2292
|
+
projectDir: config.projectDir,
|
|
2293
|
+
allowUnsupportedNpm: config.allowUnsupportedNpm
|
|
2294
|
+
});
|
|
2295
|
+
await installer.initialize();
|
|
2296
|
+
startWatcher();
|
|
2297
|
+
}
|
|
2298
|
+
cancelDisconnectMessage();
|
|
2299
|
+
if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2300
|
+
})();
|
|
2453
2301
|
});
|
|
2454
2302
|
async function handleMessage(message) {
|
|
2455
2303
|
if (!config.projectDir || !installer) {
|
|
@@ -2461,15 +2309,13 @@ async function start(config) {
|
|
|
2461
2309
|
case "request-files":
|
|
2462
2310
|
event = { type: "REQUEST_FILES" };
|
|
2463
2311
|
break;
|
|
2464
|
-
case "file-list":
|
|
2465
|
-
|
|
2466
|
-
debug(`Received file list: ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB)`);
|
|
2312
|
+
case "file-list":
|
|
2313
|
+
debug(`Received file list: ${message.files.length} files`);
|
|
2467
2314
|
event = {
|
|
2468
2315
|
type: "FILE_LIST",
|
|
2469
2316
|
files: message.files
|
|
2470
2317
|
};
|
|
2471
2318
|
break;
|
|
2472
|
-
}
|
|
2473
2319
|
case "file-change":
|
|
2474
2320
|
event = {
|
|
2475
2321
|
type: "FILE_CHANGE",
|
|
@@ -2491,7 +2337,7 @@ async function start(config) {
|
|
|
2491
2337
|
const unmatched = [];
|
|
2492
2338
|
for (const fileName of message.fileNames) if (!userActions.handleConfirmation(`delete:${fileName}`, true)) unmatched.push(fileName);
|
|
2493
2339
|
for (const fileName of unmatched) await processEvent({
|
|
2494
|
-
type: "
|
|
2340
|
+
type: "LOCAL_DELETE_APPROVED",
|
|
2495
2341
|
fileName
|
|
2496
2342
|
});
|
|
2497
2343
|
return;
|
|
@@ -2500,7 +2346,7 @@ async function start(config) {
|
|
|
2500
2346
|
for (const file of message.files) {
|
|
2501
2347
|
userActions.handleConfirmation(`delete:${file.fileName}`, false);
|
|
2502
2348
|
await processEvent({
|
|
2503
|
-
type: "
|
|
2349
|
+
type: "LOCAL_DELETE_REJECTED",
|
|
2504
2350
|
fileName: file.fileName,
|
|
2505
2351
|
content: file.content ?? ""
|
|
2506
2352
|
});
|
|
@@ -2508,7 +2354,7 @@ async function start(config) {
|
|
|
2508
2354
|
return;
|
|
2509
2355
|
case "file-synced":
|
|
2510
2356
|
event = {
|
|
2511
|
-
type: "
|
|
2357
|
+
type: "FILE_SYNCED_CONFIRMATION",
|
|
2512
2358
|
fileName: message.fileName,
|
|
2513
2359
|
remoteModifiedAt: message.remoteModifiedAt
|
|
2514
2360
|
};
|
|
@@ -2529,21 +2375,25 @@ async function start(config) {
|
|
|
2529
2375
|
warn(`Unhandled message type: ${message.type}`);
|
|
2530
2376
|
return;
|
|
2531
2377
|
}
|
|
2532
|
-
|
|
2378
|
+
await processEvent(event);
|
|
2533
2379
|
}
|
|
2534
|
-
connection.on("message",
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2380
|
+
connection.on("message", (message) => {
|
|
2381
|
+
(async () => {
|
|
2382
|
+
try {
|
|
2383
|
+
await handleMessage(message);
|
|
2384
|
+
} catch (err) {
|
|
2385
|
+
error("Error handling message:", err);
|
|
2386
|
+
}
|
|
2387
|
+
})();
|
|
2540
2388
|
});
|
|
2541
|
-
connection.on("disconnect",
|
|
2389
|
+
connection.on("disconnect", () => {
|
|
2542
2390
|
scheduleDisconnectMessage(() => {
|
|
2543
2391
|
status("Disconnected, waiting to reconnect...");
|
|
2544
2392
|
});
|
|
2545
|
-
|
|
2546
|
-
|
|
2393
|
+
(async () => {
|
|
2394
|
+
await processEvent({ type: "DISCONNECT" });
|
|
2395
|
+
userActions.cleanup();
|
|
2396
|
+
})();
|
|
2547
2397
|
});
|
|
2548
2398
|
connection.on("error", (err) => {
|
|
2549
2399
|
error("Error on WebSocket connection:", err);
|
|
@@ -2552,19 +2402,21 @@ async function start(config) {
|
|
|
2552
2402
|
const startWatcher = () => {
|
|
2553
2403
|
if (!config.filesDir || watcher) return;
|
|
2554
2404
|
watcher = initWatcher(config.filesDir);
|
|
2555
|
-
watcher.on("change",
|
|
2556
|
-
|
|
2405
|
+
watcher.on("change", (event) => {
|
|
2406
|
+
processEvent({
|
|
2557
2407
|
type: "WATCHER_EVENT",
|
|
2558
2408
|
event
|
|
2559
2409
|
});
|
|
2560
2410
|
});
|
|
2561
2411
|
};
|
|
2562
|
-
process.on("SIGINT",
|
|
2412
|
+
process.on("SIGINT", () => {
|
|
2563
2413
|
console.log();
|
|
2564
2414
|
status("Shutting down...");
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2415
|
+
(async () => {
|
|
2416
|
+
if (watcher) await watcher.close();
|
|
2417
|
+
connection.close();
|
|
2418
|
+
process.exit(0);
|
|
2419
|
+
})();
|
|
2568
2420
|
});
|
|
2569
2421
|
}
|
|
2570
2422
|
|
|
@@ -2581,7 +2433,7 @@ const program = new Command();
|
|
|
2581
2433
|
program.exitOverride((err) => {
|
|
2582
2434
|
if (err.code === "commander.missingArgument") {
|
|
2583
2435
|
console.error("Missing Project ID. Copy command via Code Link Plugin.");
|
|
2584
|
-
process.exit(err.exitCode
|
|
2436
|
+
process.exit(err.exitCode);
|
|
2585
2437
|
}
|
|
2586
2438
|
throw err;
|
|
2587
2439
|
});
|
|
@@ -2595,7 +2447,6 @@ program.name("framer-code-link").description("Sync Framer code components to you
|
|
|
2595
2447
|
process.exit(1);
|
|
2596
2448
|
}
|
|
2597
2449
|
}
|
|
2598
|
-
const isDev = process.env.NODE_ENV === "development";
|
|
2599
2450
|
if (options.logLevel) {
|
|
2600
2451
|
const level = {
|
|
2601
2452
|
debug: LogLevel.DEBUG,
|
|
@@ -2604,7 +2455,7 @@ program.name("framer-code-link").description("Sync Framer code components to you
|
|
|
2604
2455
|
error: LogLevel.ERROR
|
|
2605
2456
|
}[options.logLevel.toLowerCase()];
|
|
2606
2457
|
if (level !== void 0) setLogLevel(level);
|
|
2607
|
-
} else if (options.verbose
|
|
2458
|
+
} else if (options.verbose) setLogLevel(LogLevel.DEBUG);
|
|
2608
2459
|
const port = getPortFromHash(projectHash);
|
|
2609
2460
|
banner(version, port);
|
|
2610
2461
|
const config = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|