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.
Files changed (2) hide show
  1. package/dist/index.mjs +170 -4
  2. 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$1(entry.name)) continue;
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$1(fileName) {
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.1",
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 src/index.ts",
13
+ "build": "tsdown",
14
14
  "start": "node dist/index.mjs",
15
15
  "test": "vitest run"
16
16
  },