framer-code-link 0.18.0 → 0.20.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.
Files changed (2) hide show
  1. package/dist/index.mjs +103 -66
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -562,6 +562,7 @@ async function getOrCreateCerts() {
562
562
  if (existingKey || existingCert) await invalidateIncompleteServerBundle();
563
563
  status("Generating local certificates to connect securely. You may be asked for your password.");
564
564
  await generateCerts(mkcertPath);
565
+ status("Successfully generated certificates.");
565
566
  return {
566
567
  key: await fs.readFile(SERVER_KEY_PATH, "utf-8"),
567
568
  cert: await fs.readFile(SERVER_CERT_PATH, "utf-8")
@@ -1452,9 +1453,21 @@ async function findSkillsSourceDir() {
1452
1453
  const FETCH_TIMEOUT_MS = 6e4;
1453
1454
  const MAX_FETCH_RETRIES = 3;
1454
1455
  const MAX_CONSECUTIVE_FAILURES = 10;
1455
- const REACT_TYPES_VERSION = "18.3.12";
1456
- const REACT_DOM_TYPES_VERSION = "18.3.1";
1457
- const CORE_LIBRARIES = ["framer-motion", "framer"];
1456
+ const FRAMER_PACKAGE_NAME = "framer";
1457
+ const CORE_LIBRARIES = [
1458
+ "framer-motion",
1459
+ "framer",
1460
+ "react",
1461
+ "react-dom"
1462
+ ];
1463
+ /** Packages with pinned type versions — used by ATA's `// types:` comment syntax */
1464
+ const DEFAULT_PINNED_TYPE_VERSIONS = {
1465
+ "framer-motion": "12.34.3",
1466
+ "react": "18.2.0",
1467
+ "react-dom": "18.2.0",
1468
+ "@types/react": "18.2.0",
1469
+ "@types/react-dom": "18.2.0"
1470
+ };
1458
1471
  const JSON_EXTENSION_REGEX = /\.json$/i;
1459
1472
  /**
1460
1473
  * Packages that are officially supported for type acquisition.
@@ -1464,6 +1477,7 @@ const SUPPORTED_PACKAGES = new Set([
1464
1477
  "framer",
1465
1478
  "framer-motion",
1466
1479
  "react",
1480
+ "react-dom",
1467
1481
  "@types/react",
1468
1482
  "eventemitter3",
1469
1483
  "csstype",
@@ -1479,6 +1493,8 @@ var Installer = class {
1479
1493
  ata;
1480
1494
  processedImports = /* @__PURE__ */ new Set();
1481
1495
  initializationPromise = null;
1496
+ pinnedTypeVersions = { ...DEFAULT_PINNED_TYPE_VERSIONS };
1497
+ pinnedTypeVersionsPromise = null;
1482
1498
  constructor(config) {
1483
1499
  this.projectDir = config.projectDir;
1484
1500
  this.allowUnsupportedNpm = config.allowUnsupportedNpm ?? false;
@@ -1559,10 +1575,12 @@ var Installer = class {
1559
1575
  this.ensureSkills(),
1560
1576
  this.ensureGitignore()
1561
1577
  ]);
1578
+ this.pinnedTypeVersionsPromise = this.resolvePinnedTypeVersions();
1562
1579
  Promise.resolve().then(async () => {
1563
- await this.ensureReact18Types();
1564
- const coreImports = CORE_LIBRARIES.map((lib) => `import "${lib}";`).join("\n");
1565
- await this.ata(coreImports);
1580
+ const coreImports = await this.buildPinnedImports(CORE_LIBRARIES);
1581
+ const packageJsonDeps = this.allowUnsupportedNpm ? Object.keys(this.pinnedTypeVersions).filter((name) => !SUPPORTED_PACKAGES.has(name)) : [];
1582
+ const imports = [...coreImports, ...await this.buildPinnedImports(packageJsonDeps)].join("\n");
1583
+ await this.ata(imports);
1566
1584
  }).catch((err) => {
1567
1585
  debug("Type installation failed", err);
1568
1586
  });
@@ -1573,11 +1591,13 @@ var Installer = class {
1573
1591
  const imports = this.allowUnsupportedNpm ? allImports : allImports.filter((i) => this.isSupportedPackage(i.name));
1574
1592
  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)`);
1575
1593
  if (imports.length === 0) return;
1576
- const hash = imports.map((imp) => imp.name).sort().join(",");
1594
+ await this.pinnedTypeVersionsPromise;
1595
+ if (this.allowUnsupportedNpm) await this.resolvePackageJsonPins();
1596
+ const hash = imports.map((imp) => this.pinImport(imp.name)).sort().join(",");
1577
1597
  if (this.processedImports.has(hash)) return;
1578
1598
  this.processedImports.add(hash);
1579
1599
  debug(`Processing imports for ${fileName} (${imports.length} packages)`);
1580
- const filteredContent = this.allowUnsupportedNpm ? content : this.buildFilteredImports(imports);
1600
+ const filteredContent = this.allowUnsupportedNpm ? content : await this.buildFilteredImports(imports);
1581
1601
  try {
1582
1602
  await this.ata(filteredContent);
1583
1603
  } catch (err) {
@@ -1597,8 +1617,55 @@ var Installer = class {
1597
1617
  /**
1598
1618
  * Build synthetic import statements for ATA from filtered imports
1599
1619
  */
1600
- buildFilteredImports(imports) {
1601
- return imports.map((imp) => `import "${imp.name}";`).join("\n");
1620
+ async buildFilteredImports(imports) {
1621
+ return (await this.buildPinnedImports(imports.map((imp) => imp.name))).join("\n");
1622
+ }
1623
+ async buildPinnedImports(imports) {
1624
+ await this.pinnedTypeVersionsPromise;
1625
+ return imports.map((name) => this.pinImport(name));
1626
+ }
1627
+ async resolvePinnedTypeVersions() {
1628
+ try {
1629
+ const framerManifest = await fetchNpmPackageManifest(FRAMER_PACKAGE_NAME);
1630
+ const framerVersion = normalizePinnedVersion(framerManifest.version);
1631
+ if (framerVersion) this.pinnedTypeVersions.framer = framerVersion;
1632
+ for (const [pkg, defaultVersion] of Object.entries(DEFAULT_PINNED_TYPE_VERSIONS)) {
1633
+ const manifestDep = pkg.replace(/^@types\//, "");
1634
+ this.pinnedTypeVersions[pkg] = normalizePinnedVersion(getManifestDependencyVersion(framerManifest, manifestDep)) ?? defaultVersion;
1635
+ }
1636
+ debug(`Resolved ATA pins from ${FRAMER_PACKAGE_NAME}@${framerVersion ?? "latest"} (framer-motion ${this.pinnedTypeVersions["framer-motion"]}, react ${this.pinnedTypeVersions.react})`);
1637
+ } catch (err) {
1638
+ debug(`Falling back to default ATA pins for ${FRAMER_PACKAGE_NAME}`, err);
1639
+ }
1640
+ if (this.allowUnsupportedNpm) await this.resolvePackageJsonPins();
1641
+ }
1642
+ async resolvePackageJsonPins() {
1643
+ try {
1644
+ const pkgPath = path.join(this.projectDir, "package.json");
1645
+ const raw = await fs.readFile(pkgPath, "utf-8");
1646
+ const pkg = JSON.parse(raw);
1647
+ const allDeps = {
1648
+ ...pkg.dependencies ?? {},
1649
+ ...pkg.devDependencies ?? {}
1650
+ };
1651
+ for (const [name, range] of Object.entries(allDeps)) {
1652
+ const version = normalizePinnedVersion(range);
1653
+ if (version) this.pinnedTypeVersions[name] = version;
1654
+ }
1655
+ debug(`Resolved ${Object.keys(allDeps).length} package.json version pins`);
1656
+ } catch {
1657
+ warn("Could not read package.json for version pinning");
1658
+ }
1659
+ }
1660
+ /**
1661
+ * Build an import statement with an optional `// types:` version pin for ATA.
1662
+ * Resolves the base package name for subpath imports (e.g., "framer-motion/dist" -> "framer-motion").
1663
+ */
1664
+ pinImport(name) {
1665
+ const base = name.startsWith("@") ? name.split("/").slice(0, 2).join("/") : name.split("/")[0];
1666
+ const version = this.pinnedTypeVersions[base];
1667
+ if (version) return `import "${name}"; // types: ${version}`;
1668
+ return `import "${name}";`;
1602
1669
  }
1603
1670
  async writeTypeFile(receivedPath, code) {
1604
1671
  const normalized = receivedPath.replace(/^\//, "");
@@ -1622,7 +1689,8 @@ var Installer = class {
1622
1689
  const response = await fetch(`https://registry.npmjs.org/${pkgName}`);
1623
1690
  if (!response.ok) return;
1624
1691
  const npmData = await response.json();
1625
- const version = npmData["dist-tags"]?.latest;
1692
+ const pinnedVersion = this.pinnedTypeVersions[pkgName];
1693
+ const version = pinnedVersion ? this.findMatchingVersion(Object.keys(npmData.versions ?? {}), pinnedVersion) : npmData["dist-tags"]?.latest;
1626
1694
  if (!version || !npmData.versions?.[version]) return;
1627
1695
  const pkg = npmData.versions[version];
1628
1696
  if (pkg.exports) for (const key of Object.keys(pkg.exports)) pkg.exports[key] = fixExportTypes(pkg.exports[key]);
@@ -1630,6 +1698,17 @@ var Installer = class {
1630
1698
  await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
1631
1699
  } catch {}
1632
1700
  }
1701
+ /**
1702
+ * Find the best matching version from a list of available versions.
1703
+ * Supports exact versions ("18.2.0") — returns exact match if available.
1704
+ */
1705
+ findMatchingVersion(versions, pinned) {
1706
+ if (versions.includes(pinned)) return pinned;
1707
+ const [major, minor] = pinned.split(".");
1708
+ const prefix = `${major}.${minor}.`;
1709
+ const matching = versions.filter((v) => v.startsWith(prefix));
1710
+ return matching.length > 0 ? matching[matching.length - 1] : void 0;
1711
+ }
1633
1712
  async ensureTsConfig() {
1634
1713
  const tsconfigPath = path.join(this.projectDir, "tsconfig.json");
1635
1714
  try {
@@ -1735,61 +1814,19 @@ declare module "*.json"
1735
1814
  await fs.writeFile(gitignorePath, content);
1736
1815
  debug("Created .gitignore");
1737
1816
  }
1738
- async ensureReact18Types() {
1739
- const reactTypesDir = path.join(this.projectDir, "node_modules/@types/react");
1740
- const reactFiles = [
1741
- "package.json",
1742
- "index.d.ts",
1743
- "global.d.ts",
1744
- "jsx-runtime.d.ts",
1745
- "jsx-dev-runtime.d.ts"
1746
- ];
1747
- if (await this.hasTypePackage(reactTypesDir, REACT_TYPES_VERSION, reactFiles)) debug("📦 React types (from cache)");
1748
- else {
1749
- debug("Downloading React 18 types...");
1750
- await this.downloadTypePackage("@types/react", REACT_TYPES_VERSION, reactTypesDir, reactFiles);
1751
- }
1752
- const reactDomDir = path.join(this.projectDir, "node_modules/@types/react-dom");
1753
- const reactDomFiles = [
1754
- "package.json",
1755
- "index.d.ts",
1756
- "client.d.ts"
1757
- ];
1758
- if (await this.hasTypePackage(reactDomDir, REACT_DOM_TYPES_VERSION, reactDomFiles)) debug("📦 React DOM types (from cache)");
1759
- else await this.downloadTypePackage("@types/react-dom", REACT_DOM_TYPES_VERSION, reactDomDir, reactDomFiles);
1760
- }
1761
- async hasTypePackage(destinationDir, version, files) {
1762
- try {
1763
- const pkgJsonPath = path.join(destinationDir, "package.json");
1764
- const pkgJson = await fs.readFile(pkgJsonPath, "utf-8");
1765
- if (JSON.parse(pkgJson).version !== version) return false;
1766
- for (const file of files) {
1767
- if (file === "package.json") continue;
1768
- await fs.access(path.join(destinationDir, file));
1769
- }
1770
- return true;
1771
- } catch {
1772
- return false;
1773
- }
1774
- }
1775
- async downloadTypePackage(pkgName, version, destinationDir, files) {
1776
- const baseUrl = `https://unpkg.com/${pkgName}@${version}`;
1777
- await fs.mkdir(destinationDir, { recursive: true });
1778
- await Promise.all(files.map(async (file) => {
1779
- const destination = path.join(destinationDir, file);
1780
- try {
1781
- await fs.access(destination);
1782
- return;
1783
- } catch {}
1784
- try {
1785
- const response = await fetch(`${baseUrl}/${file}`);
1786
- if (!response.ok) return;
1787
- const content = await response.text();
1788
- await fs.writeFile(destination, content);
1789
- } catch {}
1790
- }));
1791
- }
1792
1817
  };
1818
+ function getManifestDependencyVersion(manifest, packageName) {
1819
+ return manifest.peerDependencies?.[packageName] ?? manifest.dependencies?.[packageName];
1820
+ }
1821
+ function normalizePinnedVersion(version) {
1822
+ if (!version) return void 0;
1823
+ return /\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/.exec(version)?.[0];
1824
+ }
1825
+ async function fetchNpmPackageManifest(packageName) {
1826
+ const response = await fetchWithRetry(`https://registry.npmjs.org/${packageName}/latest`);
1827
+ if (!response.ok) throw new Error(`Failed to fetch ${packageName} manifest: ${response.status}`);
1828
+ return await response.json();
1829
+ }
1793
1830
  /**
1794
1831
  * Transform package.json exports to include .d.ts type paths
1795
1832
  */
@@ -3149,7 +3186,6 @@ async function executeEffect(effect, context) {
3149
3186
  * Starts the sync controller with the given configuration
3150
3187
  */
3151
3188
  async function start(config) {
3152
- status("Waiting for Plugin connection...");
3153
3189
  const hashTracker = createHashTracker();
3154
3190
  const fileMetadataCache = new FileMetadataCache();
3155
3191
  const pendingRenameConfirmations = /* @__PURE__ */ new Map();
@@ -3191,6 +3227,7 @@ async function start(config) {
3191
3227
  info("");
3192
3228
  throw new Error("TLS certificate generation failed");
3193
3229
  }
3230
+ status("Waiting for Plugin connection...");
3194
3231
  const connection = await initConnection(config.port, certs);
3195
3232
  connection.on("handshake", (client, message) => {
3196
3233
  debug(`Received handshake: ${message.projectName} (${message.projectId})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",