framer-code-link 0.4.1 → 0.4.2
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 +170 -4
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -5,7 +5,6 @@ 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";
|
|
9
8
|
import { createHash } from "crypto";
|
|
10
9
|
import { setupTypeAcquisition } from "@typescript/ata";
|
|
11
10
|
import ts from "typescript";
|
|
@@ -419,6 +418,173 @@ function sendMessage(socket, message) {
|
|
|
419
418
|
});
|
|
420
419
|
}
|
|
421
420
|
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region ../code-link-shared/src/hash.ts
|
|
423
|
+
/**
|
|
424
|
+
* Base58 alphabet (no 0/O/I/l to avoid confusion)
|
|
425
|
+
*/
|
|
426
|
+
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
427
|
+
/**
|
|
428
|
+
* Derive a short, deterministic hash from the full Framer project hash.
|
|
429
|
+
* Uses a simple numeric hash encoded in base58 for compactness.
|
|
430
|
+
* Idempotent: if input is already the target length, returns it unchanged.
|
|
431
|
+
*/
|
|
432
|
+
function shortProjectHash(fullHash, length = 8) {
|
|
433
|
+
if (fullHash.length === length) return fullHash;
|
|
434
|
+
let h1 = 0;
|
|
435
|
+
let h2 = 0;
|
|
436
|
+
for (let i = 0; i < fullHash.length; i++) {
|
|
437
|
+
const char = fullHash.charCodeAt(i);
|
|
438
|
+
h1 = Math.imul(h1 ^ char, 2246822507);
|
|
439
|
+
h2 = Math.imul(h2 ^ char, 3266489909);
|
|
440
|
+
}
|
|
441
|
+
h1 ^= h2 >>> 16;
|
|
442
|
+
h2 ^= h1 >>> 13;
|
|
443
|
+
let result = "";
|
|
444
|
+
const combined = [Math.abs(h1), Math.abs(h2)];
|
|
445
|
+
for (const num of combined) {
|
|
446
|
+
let n = num >>> 0;
|
|
447
|
+
while (n > 0 && result.length < length) {
|
|
448
|
+
result += BASE58[n % 58];
|
|
449
|
+
n = Math.floor(n / 58);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
while (result.length < length) result += BASE58[0];
|
|
453
|
+
return result.slice(0, length);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region ../code-link-shared/src/ports.ts
|
|
458
|
+
/**
|
|
459
|
+
* Generate a deterministic port number from a project hash (full or short).
|
|
460
|
+
* Port range: 3847-4096 (250 possible ports)
|
|
461
|
+
* Must match between CLI and plugin.
|
|
462
|
+
*
|
|
463
|
+
* Internally normalizes to the short id so both full and short inputs yield the same port.
|
|
464
|
+
*/
|
|
465
|
+
function getPortFromHash(projectHash) {
|
|
466
|
+
const shortId = shortProjectHash(projectHash);
|
|
467
|
+
let hash = 0;
|
|
468
|
+
for (let i = 0; i < shortId.length; i++) {
|
|
469
|
+
const char = shortId.charCodeAt(i);
|
|
470
|
+
hash = (hash << 5) - hash + char;
|
|
471
|
+
hash = hash & hash;
|
|
472
|
+
}
|
|
473
|
+
return 3847 + Math.abs(hash) % 250;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region ../code-link-shared/src/paths.ts
|
|
478
|
+
/**
|
|
479
|
+
* File path normalization utilities
|
|
480
|
+
* Framer code files include extensions in their paths (.tsx, .ts, etc.)
|
|
481
|
+
*/
|
|
482
|
+
const firstCharacterRegex = /^[a-zA-Z$_]/;
|
|
483
|
+
const remainingCharactersRegex = /[^a-zA-Z0-9$_]/g;
|
|
484
|
+
const onlyDotsRegex = /^\.+$/;
|
|
485
|
+
const tsxExtension = ".tsx";
|
|
486
|
+
var NameType = /* @__PURE__ */ function(NameType$1) {
|
|
487
|
+
NameType$1["Variable"] = "Variable";
|
|
488
|
+
NameType$1["Selector"] = "Selector";
|
|
489
|
+
NameType$1["Directory"] = "Directory";
|
|
490
|
+
return NameType$1;
|
|
491
|
+
}(NameType || {});
|
|
492
|
+
function sanitizedName(type, name) {
|
|
493
|
+
if (!name) return null;
|
|
494
|
+
let validName = name.trim();
|
|
495
|
+
if (validName.length === 0) return null;
|
|
496
|
+
const validFirstChar = type === NameType.Selector ? "_" : "$";
|
|
497
|
+
if (type === NameType.Directory) {
|
|
498
|
+
if (onlyDotsRegex.test(validName)) return null;
|
|
499
|
+
} else if (!firstCharacterRegex.test(validName)) validName = validFirstChar + validName;
|
|
500
|
+
validName = validName.replace(remainingCharactersRegex, "_");
|
|
501
|
+
validName = validName.replace(/_+/g, "_");
|
|
502
|
+
validName = validName.replace(/^\$_/u, validFirstChar);
|
|
503
|
+
return validName;
|
|
504
|
+
}
|
|
505
|
+
function sanitizedVariableName(name) {
|
|
506
|
+
return sanitizedName(NameType.Variable, name);
|
|
507
|
+
}
|
|
508
|
+
function sanitizedDirectoryName(name) {
|
|
509
|
+
return sanitizedName(NameType.Directory, name);
|
|
510
|
+
}
|
|
511
|
+
function capitalizeFirstLetter(str) {
|
|
512
|
+
if (str.length === 0) return str;
|
|
513
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
514
|
+
}
|
|
515
|
+
function hasValidExtension(fileName) {
|
|
516
|
+
if (fileName.endsWith(".json")) return true;
|
|
517
|
+
return /\.[tj]sx?$/u.test(fileName);
|
|
518
|
+
}
|
|
519
|
+
function splitExtension(fileName) {
|
|
520
|
+
const lastDot = fileName.lastIndexOf(".");
|
|
521
|
+
if (lastDot <= 0) return [fileName, ""];
|
|
522
|
+
return [fileName.slice(0, lastDot), fileName.slice(lastDot + 1)];
|
|
523
|
+
}
|
|
524
|
+
function dirname(filePath) {
|
|
525
|
+
const at = filePath.lastIndexOf("/");
|
|
526
|
+
if (at < 0) return "";
|
|
527
|
+
return filePath.slice(0, at);
|
|
528
|
+
}
|
|
529
|
+
function filename(filePath) {
|
|
530
|
+
const at = filePath.lastIndexOf("/") + 1;
|
|
531
|
+
return filePath.slice(at);
|
|
532
|
+
}
|
|
533
|
+
function pathJoin(...parts) {
|
|
534
|
+
let res = "";
|
|
535
|
+
parts.forEach((part) => {
|
|
536
|
+
while (part.startsWith("/")) part = part.slice(1);
|
|
537
|
+
while (part.endsWith("/")) part = part.slice(0, -1);
|
|
538
|
+
if (part === "") return;
|
|
539
|
+
if (res !== "") res += "/";
|
|
540
|
+
res += part;
|
|
541
|
+
});
|
|
542
|
+
return res;
|
|
543
|
+
}
|
|
544
|
+
function normalizePath(filePath) {
|
|
545
|
+
if (!filePath) return "";
|
|
546
|
+
const isAbsolute = filePath.startsWith("/");
|
|
547
|
+
const segments = filePath.replace(/\\/g, "/").split("/");
|
|
548
|
+
const stack = [];
|
|
549
|
+
for (const segment of segments) {
|
|
550
|
+
if (!segment || segment === ".") continue;
|
|
551
|
+
if (segment === "..") {
|
|
552
|
+
if (stack.length > 0) stack.pop();
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
stack.push(segment);
|
|
556
|
+
}
|
|
557
|
+
const normalized = stack.join("/");
|
|
558
|
+
if (isAbsolute) return `/${normalized}`;
|
|
559
|
+
return normalized;
|
|
560
|
+
}
|
|
561
|
+
function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
562
|
+
const trimmed = input.trim();
|
|
563
|
+
const [inputName, extension] = splitExtension(filename(trimmed));
|
|
564
|
+
const extensionWithDot = extension ? `.${extension}` : "";
|
|
565
|
+
const dirName = dirname(trimmed).split("/").map((part) => sanitizedDirectoryName(part)).filter((part) => Boolean(part)).join("/");
|
|
566
|
+
let name = sanitizedVariableName(inputName) ?? "MyComponent";
|
|
567
|
+
if ((!hasValidExtension(extension) || extension === tsxExtension) && capitalizeReactComponent) name = capitalizeFirstLetter(name);
|
|
568
|
+
return {
|
|
569
|
+
path: pathJoin(dirName, name + extensionWithDot),
|
|
570
|
+
dirName,
|
|
571
|
+
name,
|
|
572
|
+
extension
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
function isSupportedExtension$1(filePath) {
|
|
576
|
+
return /\.(tsx?|jsx?|json)$/i.test(filePath);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Pluralize a word based on count
|
|
580
|
+
* @example pluralize(1, "file") => "1 file"
|
|
581
|
+
* @example pluralize(3, "file") => "3 files"
|
|
582
|
+
* @example pluralize(0, "conflict") => "0 conflicts"
|
|
583
|
+
*/
|
|
584
|
+
function pluralize(count, singular, plural) {
|
|
585
|
+
return `${count} ${count === 1 ? singular : plural ?? `${singular}s`}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
422
588
|
//#endregion
|
|
423
589
|
//#region src/utils/node-paths.ts
|
|
424
590
|
/**
|
|
@@ -473,7 +639,7 @@ function initWatcher(filesDir) {
|
|
|
473
639
|
});
|
|
474
640
|
debug(`Watching directory: ${filesDir}`);
|
|
475
641
|
const emitEvent = async (kind, absolutePath) => {
|
|
476
|
-
if (!isSupportedExtension(absolutePath)) return;
|
|
642
|
+
if (!isSupportedExtension$1(absolutePath)) return;
|
|
477
643
|
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
478
644
|
const relativePath = sanitizeFilePath(rawRelativePath, false).path;
|
|
479
645
|
let effectiveAbsolutePath = absolutePath;
|
|
@@ -637,7 +803,7 @@ async function listFiles(filesDir) {
|
|
|
637
803
|
await walk(entryPath);
|
|
638
804
|
continue;
|
|
639
805
|
}
|
|
640
|
-
if (!isSupportedExtension
|
|
806
|
+
if (!isSupportedExtension(entry.name)) continue;
|
|
641
807
|
const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
|
|
642
808
|
try {
|
|
643
809
|
const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
|
|
@@ -888,7 +1054,7 @@ function sanitizeRelativePath(relativePath) {
|
|
|
888
1054
|
extension: sanitized.extension || path.extname(normalized) || DEFAULT_EXTENSION
|
|
889
1055
|
};
|
|
890
1056
|
}
|
|
891
|
-
function isSupportedExtension
|
|
1057
|
+
function isSupportedExtension(fileName) {
|
|
892
1058
|
const lower = fileName.toLowerCase();
|
|
893
1059
|
return SUPPORTED_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
894
1060
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "CLI tool for syncing Framer code components - controller-centric architecture",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"dev": "NODE_ENV=development tsx src/index.ts",
|
|
13
|
-
"build": "tsdown
|
|
13
|
+
"build": "tsdown",
|
|
14
14
|
"start": "node dist/index.mjs",
|
|
15
15
|
"test": "vitest run"
|
|
16
16
|
},
|