framer-code-link 0.21.0-alpha.2 → 0.21.0-alpha.4

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 +108 -206
  2. package/package.json +2 -1
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { Command, InvalidArgumentError } from "commander";
3
+ import { Command, InvalidArgumentError, Option } from "commander";
4
4
  import fs from "fs/promises";
5
5
  import path from "path";
6
6
  import { execFile } from "node:child_process";
@@ -916,32 +916,31 @@ const DEFAULT_REMOTE_DRIFT_MS = 2e3;
916
916
  */
917
917
  async function listFiles(filesDir) {
918
918
  const files = [];
919
- async function walk(currentDir) {
920
- const entries = await fs.readdir(currentDir, { withFileTypes: true });
921
- for (const entry of entries) {
922
- const entryPath = path.join(currentDir, entry.name);
923
- if (entry.isDirectory()) {
924
- await walk(entryPath);
925
- continue;
926
- }
927
- if (!isSupportedExtension(entry.name)) continue;
928
- const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
929
- try {
930
- const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
931
- files.push({
932
- name: sanitizedPath,
933
- content,
934
- modifiedAt: stats.mtimeMs
935
- });
936
- } catch (err) {
937
- warn(`Failed to read ${entryPath}:`, err);
938
- }
939
- }
940
- }
919
+ let entries;
941
920
  try {
942
- await walk(filesDir);
921
+ entries = await fs.readdir(filesDir, {
922
+ withFileTypes: true,
923
+ recursive: true
924
+ });
943
925
  } catch (err) {
944
926
  warn("Failed to list files:", err);
927
+ return files;
928
+ }
929
+ for (const entry of entries) {
930
+ if (!entry.isFile()) continue;
931
+ if (!isSupportedExtension(entry.name)) continue;
932
+ const entryPath = path.join(entry.parentPath, entry.name);
933
+ const sanitizedPath = sanitizeFilePath(normalizePath(path.relative(filesDir, entryPath)), false).path;
934
+ try {
935
+ const [content, stats] = await Promise.all([fs.readFile(entryPath, "utf-8"), fs.stat(entryPath)]);
936
+ files.push({
937
+ name: sanitizedPath,
938
+ content,
939
+ modifiedAt: stats.mtimeMs
940
+ });
941
+ } catch (err) {
942
+ warn(`Failed to read ${entryPath}:`, err);
943
+ }
945
944
  }
946
945
  return files;
947
946
  }
@@ -1288,49 +1287,6 @@ function tryGitInit(projectDir) {
1288
1287
  }
1289
1288
  }
1290
1289
 
1291
- //#endregion
1292
- //#region src/utils/imports.ts
1293
- /**
1294
- * Extract npm and URL-based imports from source code.
1295
- */
1296
- function extractImports(code) {
1297
- const imports = [];
1298
- const seen = /* @__PURE__ */ new Set();
1299
- const npmRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]([^./][^'"]+)['"]/g;
1300
- const urlRegex = /import\s+(?:(?:\*\s+as\s+\w+)|(?:\w+)|(?:\{[^}]*\}))\s+from\s+['"]https?:\/\/[^'"]+['"]/g;
1301
- let match;
1302
- while ((match = npmRegex.exec(code)) !== null) {
1303
- const pkgName = match[1];
1304
- const normalized = pkgName.startsWith("@") ? pkgName.split("/").slice(0, 2).join("/") : pkgName.split("/")[0];
1305
- if (!seen.has(normalized)) {
1306
- seen.add(normalized);
1307
- imports.push({
1308
- type: "npm",
1309
- name: normalized,
1310
- raw: match[0]
1311
- });
1312
- }
1313
- }
1314
- while ((match = urlRegex.exec(code)) !== null) {
1315
- const pkgName = extractPackageFromUrl(match[0]);
1316
- if (pkgName && !seen.has(pkgName)) {
1317
- seen.add(pkgName);
1318
- imports.push({
1319
- type: "url",
1320
- name: pkgName,
1321
- raw: match[0]
1322
- });
1323
- }
1324
- }
1325
- return imports;
1326
- }
1327
- /**
1328
- * Attempt to derive an npm-style package specifier from a URL import.
1329
- */
1330
- function extractPackageFromUrl(url) {
1331
- return /\/(@?[^@/]+(?:\/[^@/]+)?)/.exec(url)?.[1] ?? null;
1332
- }
1333
-
1334
1290
  //#endregion
1335
1291
  //#region src/helpers/skills.ts
1336
1292
  /**
@@ -1464,10 +1420,9 @@ const DEFAULT_PINNED_TYPE_VERSIONS = {
1464
1420
  };
1465
1421
  const JSON_EXTENSION_REGEX = /\.json$/i;
1466
1422
  /**
1467
- * Packages that are officially supported for type acquisition.
1468
- * Use --unsupported-npm flag to allow other packages.
1423
+ * Packages that receive ATA types even when unsupported npm is not enabled.
1469
1424
  */
1470
- const SUPPORTED_PACKAGES = new Set([
1425
+ const DEFAULT_PACKAGES = new Set([
1471
1426
  "framer",
1472
1427
  "framer-motion",
1473
1428
  "react",
@@ -1487,14 +1442,12 @@ var Installer = class {
1487
1442
  requestDependencyVersions;
1488
1443
  ata;
1489
1444
  processedImports = /* @__PURE__ */ new Set();
1490
- packageManagerPackages = /* @__PURE__ */ new Set();
1491
- packageJsonRefreshPromise = Promise.resolve();
1492
1445
  initializationPromise = null;
1493
1446
  pinnedTypeVersions = { ...DEFAULT_PINNED_TYPE_VERSIONS };
1494
1447
  pinnedTypeVersionsPromise = null;
1495
1448
  constructor(config) {
1496
1449
  this.projectDir = config.projectDir;
1497
- this.npmStrategy = config.npmStrategy ?? "none";
1450
+ this.npmStrategy = config.npmStrategy;
1498
1451
  this.requestDependencyVersions = config.requestDependencyVersions ?? (async (packages) => Object.fromEntries(packages.map((packageName) => [packageName, null])));
1499
1452
  const seenPackages = /* @__PURE__ */ new Set();
1500
1453
  this.ata = setupTypeAcquisition({
@@ -1553,16 +1506,21 @@ var Installer = class {
1553
1506
  return this.initializationPromise;
1554
1507
  }
1555
1508
  /**
1556
- * Fire-and-forget processing of a component file to fetch missing types.
1509
+ * Process component files to fetch missing types or add `package.json` dependencies.
1557
1510
  * JSON files are ignored.
1558
1511
  */
1559
- process(fileName, content) {
1560
- if (!content || JSON_EXTENSION_REGEX.test(fileName)) return;
1561
- Promise.resolve().then(async () => {
1562
- await this.processImports(fileName, content);
1563
- }).catch((err) => {
1564
- debug(`Type installer failed for ${fileName}`, err);
1565
- });
1512
+ async processFiles(files) {
1513
+ const packageNames = /* @__PURE__ */ new Set();
1514
+ for (const file of files) {
1515
+ if (!file.content || JSON_EXTENSION_REGEX.test(file.name)) continue;
1516
+ try {
1517
+ const imports = extractNpmPackageNames(file.content);
1518
+ for (const packageName of imports) packageNames.add(packageName);
1519
+ } catch (err) {
1520
+ debug(`Type installer failed to parse imports for ${file.name}`, err);
1521
+ }
1522
+ }
1523
+ await this.processNpmPackages(packageNames, files.length);
1566
1524
  }
1567
1525
  async initializeProject() {
1568
1526
  await Promise.all([
@@ -1575,83 +1533,48 @@ var Installer = class {
1575
1533
  ]);
1576
1534
  if (this.npmStrategy === "package-manager") {
1577
1535
  await this.resolvePinnedTypeVersions();
1578
- await this.enqueuePackageJsonRefresh(await this.collectPackageManagerPackageNames());
1579
1536
  return;
1580
1537
  }
1581
1538
  this.pinnedTypeVersionsPromise = this.resolvePinnedTypeVersions();
1582
1539
  Promise.resolve().then(async () => {
1583
- const coreImports = await this.buildPinnedImports(CORE_LIBRARIES);
1584
- const packageJsonDeps = this.npmStrategy === "acquire-types" ? Object.keys(this.pinnedTypeVersions).filter((name) => !SUPPORTED_PACKAGES.has(name)) : [];
1585
- const imports = [...coreImports, ...await this.buildPinnedImports(packageJsonDeps)].join("\n");
1586
- await this.ata(imports);
1540
+ await this.ata((await this.buildPinnedImports(CORE_LIBRARIES)).join("\n"));
1587
1541
  }).catch((err) => {
1588
1542
  debug("Type installation failed", err);
1589
1543
  });
1590
1544
  }
1591
- async processImports(fileName, content) {
1592
- const allImports = extractImports(content).filter((i) => i.type === "npm");
1593
- if (allImports.length === 0) return;
1545
+ async processNpmPackages(packageNames, fileCount) {
1546
+ const allPackageNames = [...packageNames];
1594
1547
  if (this.npmStrategy === "package-manager") {
1595
- await this.enqueuePackageJsonRefresh(allImports.map((imp) => getBasePackageName(imp.name)));
1548
+ for (const packageName of [...CORE_LIBRARIES, ...PACKAGE_MANAGER_DEV_DEPENDENCIES]) allPackageNames.push(packageName);
1549
+ try {
1550
+ await this.updatePackageJsonDependencies(allPackageNames);
1551
+ } catch (err) {
1552
+ warn("Could not update package.json dependency versions", err);
1553
+ }
1596
1554
  return;
1597
1555
  }
1598
- const imports = this.npmStrategy === "acquire-types" ? allImports : allImports.filter((i) => this.isSupportedPackage(i.name));
1599
- if (allImports.length - imports.length > 0 && this.npmStrategy !== "acquire-types") debug(`Skipping unsupported packages: ${allImports.filter((i) => !this.isSupportedPackage(i.name)).map((i) => i.name).join(", ")} (use --unsupported-npm to enable)`);
1600
- if (imports.length === 0) return;
1556
+ if (allPackageNames.length === 0) return;
1557
+ let packagesForAta;
1558
+ if (this.npmStrategy === "acquire-types") packagesForAta = allPackageNames;
1559
+ else {
1560
+ packagesForAta = allPackageNames.filter((packageName) => this.isDefaultPackage(packageName));
1561
+ const unsupportedPackages = allPackageNames.filter((packageName) => !this.isDefaultPackage(packageName));
1562
+ if (unsupportedPackages.length > 0) debug(`Skipping unsupported packages: ${unsupportedPackages.join(", ")} (use --unsupported-npm to enable)`);
1563
+ }
1564
+ if (packagesForAta.length === 0) return;
1601
1565
  await this.pinnedTypeVersionsPromise;
1602
- if (this.npmStrategy === "acquire-types") await this.resolvePackageJsonPins();
1603
- const hash = imports.map((imp) => this.pinImport(imp.name)).sort().join(",");
1566
+ const hash = packagesForAta.map((packageName) => this.pinImport(packageName)).sort().join(",");
1604
1567
  if (this.processedImports.has(hash)) return;
1605
1568
  this.processedImports.add(hash);
1606
- debug(`Processing imports for ${fileName} (${imports.length} packages)`);
1607
- const filteredContent = this.npmStrategy === "acquire-types" ? content : await this.buildFilteredImports(imports);
1569
+ debug(`Processing imports from ${fileCount} ${fileCount === 1 ? "file" : "files"} (${packagesForAta.length} packages)`);
1608
1570
  try {
1609
- await this.ata(filteredContent);
1571
+ await this.ata((await this.buildPinnedImports(packagesForAta)).join("\n"));
1610
1572
  } catch (err) {
1611
- warn(`Type fetching failed for ${fileName}`);
1612
- debug(`ATA error for ${fileName}:`, err);
1573
+ warn("Type fetching failed");
1574
+ debug("ATA error:", err);
1613
1575
  }
1614
1576
  }
1615
- async collectPackageManagerPackageNames() {
1616
- const packageNames = new Set([...CORE_LIBRARIES, ...PACKAGE_MANAGER_DEV_DEPENDENCIES]);
1617
- await this.addPackageNamesFromDirectory(path.join(this.projectDir, "files"), packageNames);
1618
- return [...packageNames];
1619
- }
1620
- async addPackageNamesFromDirectory(directory, packageNames) {
1621
- let entries;
1622
- try {
1623
- entries = await fs.readdir(directory, { withFileTypes: true });
1624
- } catch {
1625
- return;
1626
- }
1627
- await Promise.all(entries.map(async (entry) => {
1628
- const entryPath = path.join(directory, entry.name);
1629
- if (entry.isDirectory()) {
1630
- await this.addPackageNamesFromDirectory(entryPath, packageNames);
1631
- return;
1632
- }
1633
- if (!entry.isFile() || JSON_EXTENSION_REGEX.test(entry.name)) return;
1634
- try {
1635
- const content = await fs.readFile(entryPath, "utf-8");
1636
- for (const imported of extractImports(content).filter((i) => i.type === "npm")) packageNames.add(getBasePackageName(imported.name));
1637
- } catch {}
1638
- }));
1639
- }
1640
- async enqueuePackageJsonRefresh(packageNames) {
1641
- const missingPackageNames = packageNames.map((name) => getBasePackageName(name)).filter((name) => {
1642
- if (this.packageManagerPackages.has(name)) return false;
1643
- this.packageManagerPackages.add(name);
1644
- return true;
1645
- });
1646
- if (missingPackageNames.length === 0) return this.packageJsonRefreshPromise;
1647
- this.packageJsonRefreshPromise = this.packageJsonRefreshPromise.then(async () => {
1648
- await this.refreshPackageJsonFromPlugin(missingPackageNames);
1649
- }).catch((err) => {
1650
- warn("Could not refresh package.json dependency versions", err);
1651
- });
1652
- return this.packageJsonRefreshPromise;
1653
- }
1654
- async refreshPackageJsonFromPlugin(packageNames) {
1577
+ async updatePackageJsonDependencies(packageNames) {
1655
1578
  const uniquePackageNames = [...new Set(packageNames)].sort();
1656
1579
  const versions = await this.requestDependencyVersions(uniquePackageNames);
1657
1580
  const packagePath = path.join(this.projectDir, "package.json");
@@ -1661,7 +1584,7 @@ var Installer = class {
1661
1584
  const devDependencies = typeof pkg.devDependencies === "object" && pkg.devDependencies !== null && !Array.isArray(pkg.devDependencies) ? { ...pkg.devDependencies } : {};
1662
1585
  let changed = false;
1663
1586
  for (const packageName of uniquePackageNames) {
1664
- const version = (versions[packageName] ?? this.pinnedTypeVersions[packageName])?.replace(/^\^/, "");
1587
+ const version = versions[packageName] ?? this.pinnedTypeVersions[packageName];
1665
1588
  if (!version) continue;
1666
1589
  const targetDependencies = PACKAGE_MANAGER_DEV_DEPENDENCIES.includes(packageName) ? devDependencies : dependencies;
1667
1590
  const oppositeDependencies = PACKAGE_MANAGER_DEV_DEPENDENCIES.includes(packageName) ? dependencies : devDependencies;
@@ -1678,23 +1601,16 @@ var Installer = class {
1678
1601
  pkg.dependencies = sortDependencyMap(dependencies);
1679
1602
  pkg.devDependencies = sortDependencyMap(devDependencies);
1680
1603
  await fs.writeFile(packagePath, JSON.stringify(pkg, null, 4));
1681
- status("Synced project dependencies to package.json. Run your package manager to install them.");
1604
+ status("Updated dependencies. Run your package manager to install them.");
1682
1605
  debug(`Updated package.json dependency versions for ${uniquePackageNames.join(", ")}`);
1683
1606
  }
1684
1607
  /**
1685
- * Check if a package is in the supported list.
1686
- * Also checks for subpath imports (e.g., "framer/build" -> "framer")
1608
+ * Check if a package receives ATA types without unsupported npm enabled.
1687
1609
  */
1688
- isSupportedPackage(pkgName) {
1689
- if (SUPPORTED_PACKAGES.has(pkgName)) return true;
1610
+ isDefaultPackage(pkgName) {
1611
+ if (DEFAULT_PACKAGES.has(pkgName)) return true;
1690
1612
  const basePkg = getBasePackageName(pkgName);
1691
- return SUPPORTED_PACKAGES.has(basePkg);
1692
- }
1693
- /**
1694
- * Build synthetic import statements for ATA from filtered imports
1695
- */
1696
- async buildFilteredImports(imports) {
1697
- return (await this.buildPinnedImports(imports.map((imp) => imp.name))).join("\n");
1613
+ return DEFAULT_PACKAGES.has(basePkg);
1698
1614
  }
1699
1615
  async buildPinnedImports(imports) {
1700
1616
  await this.pinnedTypeVersionsPromise;
@@ -1713,25 +1629,6 @@ var Installer = class {
1713
1629
  } catch (err) {
1714
1630
  debug(`Falling back to default ATA pins for ${FRAMER_PACKAGE_NAME}`, err);
1715
1631
  }
1716
- if (this.npmStrategy === "acquire-types") await this.resolvePackageJsonPins();
1717
- }
1718
- async resolvePackageJsonPins() {
1719
- try {
1720
- const pkgPath = path.join(this.projectDir, "package.json");
1721
- const raw = await fs.readFile(pkgPath, "utf-8");
1722
- const pkg = JSON.parse(raw);
1723
- const allDeps = {
1724
- ...pkg.dependencies ?? {},
1725
- ...pkg.devDependencies ?? {}
1726
- };
1727
- for (const [name, range] of Object.entries(allDeps)) {
1728
- const version = normalizePinnedVersion(range);
1729
- if (version) this.pinnedTypeVersions[name] = version;
1730
- }
1731
- debug(`Resolved ${Object.keys(allDeps).length} package.json version pins`);
1732
- } catch {
1733
- warn("Could not read package.json for version pinning");
1734
- }
1735
1632
  }
1736
1633
  /**
1737
1634
  * Build an import statement with an optional `// types:` version pin for ATA.
@@ -1899,6 +1796,15 @@ function getBasePackageName(packageName) {
1899
1796
  if (packageName.startsWith("@")) return parts.length >= 2 ? parts.slice(0, 2).join("/") : packageName;
1900
1797
  return parts[0] ?? packageName;
1901
1798
  }
1799
+ function extractNpmPackageNames(code) {
1800
+ const { importedFiles } = ts.preProcessFile(code, true, true);
1801
+ const seen = /* @__PURE__ */ new Set();
1802
+ for (const { fileName } of importedFiles) if (isPackageSpecifier(fileName)) seen.add(getBasePackageName(fileName));
1803
+ return [...seen];
1804
+ }
1805
+ function isPackageSpecifier(specifier) {
1806
+ return !specifier.startsWith(".") && !specifier.startsWith("/") && !/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(specifier);
1807
+ }
1902
1808
  function sortDependencyMap(dependencies) {
1903
1809
  return Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)));
1904
1810
  }
@@ -2097,12 +2003,6 @@ async function matchesProject(packageJsonPath, projectHash) {
2097
2003
  //#endregion
2098
2004
  //#region src/helpers/npm-strategy.ts
2099
2005
  const CONFIG_FIELD = "codeLink.npmStrategy";
2100
- const LOCKFILES = [
2101
- "yarn.lock",
2102
- "pnpm-lock.yaml",
2103
- "package-lock.json",
2104
- "bun.lockb"
2105
- ];
2106
2006
  async function resolveNpmStrategy(config, projectDir) {
2107
2007
  if (config.npmStrategy) {
2108
2008
  debug(`Using npm strategy from CLI flag: ${config.npmStrategy}`);
@@ -2113,13 +2013,7 @@ async function resolveNpmStrategy(config, projectDir) {
2113
2013
  debug(`Using npm strategy from package.json ${CONFIG_FIELD}: ${packageJsonStrategy}`);
2114
2014
  return packageJsonStrategy;
2115
2015
  }
2116
- const detectedLockfile = await detectLockfile(projectDir);
2117
- if (detectedLockfile) {
2118
- debug(`Using npm strategy package-manager from ${detectedLockfile}`);
2119
- return "package-manager";
2120
- }
2121
- debug("Using default npm strategy: none");
2122
- return "none";
2016
+ debug("No npm strategy from CLI or package.json");
2123
2017
  }
2124
2018
  async function readPackageJsonStrategy(projectDir) {
2125
2019
  try {
@@ -2134,15 +2028,8 @@ async function readPackageJsonStrategy(projectDir) {
2134
2028
  return null;
2135
2029
  }
2136
2030
  }
2137
- async function detectLockfile(projectDir) {
2138
- for (const fileName of LOCKFILES) try {
2139
- await fs.access(path.join(projectDir, fileName));
2140
- return fileName;
2141
- } catch {}
2142
- return null;
2143
- }
2144
2031
  function isNpmStrategy(value) {
2145
- return value === "none" || value === "acquire-types" || value === "package-manager";
2032
+ return value === "acquire-types" || value === "package-manager";
2146
2033
  }
2147
2034
 
2148
2035
  //#endregion
@@ -3426,12 +3313,14 @@ async function writeFiles(files, ctx, options) {
3426
3313
  const filesToWrite = options.echoPolicy === "skip-expected-echoes" ? filterEchoedFiles(files, runtime.memory) : files;
3427
3314
  if (options.echoPolicy === "skip-expected-echoes" && filesToWrite.length !== files.length) debug(`Skipped ${pluralize(files.length - filesToWrite.length, "echoed change")}`);
3428
3315
  const results = await writeRemoteFiles(filesToWrite, runtime.workspace.filesDir, runtime.memory);
3316
+ const installerFiles = [];
3429
3317
  for (const result of results) {
3430
3318
  if (!result.ok) continue;
3431
3319
  if (!options.silent) fileDown(result.path);
3432
3320
  runtime.memory.recordSyncedContent(result.path, result.file.content, result.file.modifiedAt ?? Date.now());
3433
- runtime.installer?.process(result.path, result.file.content);
3321
+ if (!options.silent) installerFiles.push(result.file);
3434
3322
  }
3323
+ await processInstallerFiles(runtime, installerFiles);
3435
3324
  }
3436
3325
  async function deleteFiles(fileNames, ctx) {
3437
3326
  const { runtime } = ctx;
@@ -3458,7 +3347,10 @@ async function sendLocalChange(fileName, content, ctx) {
3458
3347
  })) return;
3459
3348
  runtime.memory.armContentEcho(fileName, content);
3460
3349
  fileUp(fileName);
3461
- runtime.installer?.process(fileName, content);
3350
+ await processInstallerFiles(runtime, [{
3351
+ name: fileName,
3352
+ content
3353
+ }]);
3462
3354
  }
3463
3355
  async function sendFileDelete(fileNames, ctx) {
3464
3356
  if (fileNames.length === 0) return;
@@ -3554,19 +3446,22 @@ async function applySyncComplete(effect, ctx) {
3554
3446
  const wasDisconnected = runtime.disconnectUi.wasRecentlyDisconnected();
3555
3447
  let shouldShutdown = !!config.once;
3556
3448
  let shouldTryGitInit = false;
3449
+ let statusMessage = null;
3557
3450
  if (wasDisconnected) {
3558
3451
  const didShow = runtime.disconnectUi.didShowNotice();
3559
3452
  shouldShutdown = didShow && !!config.once;
3560
3453
  if (didShow) {
3561
3454
  success(`Reconnected, synced ${pluralize(effect.totalCount, "file")} (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
3562
- status(syncCompleteStatusMessage(config));
3455
+ statusMessage = syncCompleteStatusMessage(config);
3563
3456
  }
3564
3457
  } else {
3565
3458
  const message = syncCompleteSuccessMessage(runtime, effect);
3566
3459
  if (message) success(message);
3567
- status(syncCompleteStatusMessage(config));
3460
+ statusMessage = syncCompleteStatusMessage(config);
3568
3461
  shouldTryGitInit = !!(runtime.workspace.projectDirCreated && runtime.workspace.projectDir);
3569
3462
  }
3463
+ await processAllInstallerFiles(ctx);
3464
+ if (statusMessage) status(statusMessage);
3570
3465
  await sendToPlugin(syncState.socket, {
3571
3466
  type: "sync-status",
3572
3467
  status: "ready"
@@ -3576,6 +3471,21 @@ async function applySyncComplete(effect, ctx) {
3576
3471
  if (shouldTryGitInit && runtime.workspace.projectDir) tryGitInit(runtime.workspace.projectDir);
3577
3472
  if (shouldShutdown) await shutdown();
3578
3473
  }
3474
+ async function processInstallerFiles(runtime, files) {
3475
+ if (!runtime.installer || runtime.lastEmittedSyncStatus !== "ready") return;
3476
+ if (files.length === 0) return;
3477
+ try {
3478
+ await runtime.installer.processFiles(files);
3479
+ } catch (err) {
3480
+ debug("Type installer failed", err);
3481
+ }
3482
+ }
3483
+ async function processAllInstallerFiles(ctx) {
3484
+ const { runtime } = ctx;
3485
+ if (!runtime.installer || !runtime.workspace.filesDir) return;
3486
+ const files = await listFiles(runtime.workspace.filesDir);
3487
+ await runtime.installer.processFiles(files);
3488
+ }
3579
3489
  async function flushPendingSyncComplete(ctx) {
3580
3490
  const result = ctx.runtime.claimPendingSyncComplete();
3581
3491
  if (result.status === "ready") await applySyncComplete({
@@ -4076,18 +3986,10 @@ async function start(config) {
4076
3986
  const { version } = createRequire(import.meta.url)("../package.json");
4077
3987
  const program = new Command();
4078
3988
  function parseUnsupportedNpmMode(mode) {
4079
- if (mode === void 0) return "acquire-types";
4080
3989
  if (mode === "acquire-types" || mode === "package-manager") return mode;
4081
3990
  throw new InvalidArgumentError("unsupported npm mode must be 'acquire-types' or 'package-manager'");
4082
3991
  }
4083
- program.exitOverride((err) => {
4084
- if (err.code === "commander.missingArgument") {
4085
- console.error("Missing Project ID. Copy command via Code Link Plugin.");
4086
- process.exit(err.exitCode);
4087
- }
4088
- throw err;
4089
- });
4090
- program.name("framer-code-link").description("Sync Framer code components to your local filesystem").version(version).argument("[projectHash]", "Framer Project ID Hash (auto-detected from package.json if omitted)").option("-n, --name <name>", "Project name (optional)").option("-d, --dir <directory>", "Explicit project directory").option("--once", "Exit after the initial sync completes").option("-v, --verbose", "Enable verbose logging").option("--log-level <level>", "Set log level (debug, info, warn, error)").option("--dangerously-auto-delete", "Automatically delete remote files without confirmation").option("--unsupported-npm [mode]", "Handle unsupported npm packages (acquire-types or package-manager)", parseUnsupportedNpmMode).action(async (projectHash, options) => {
3992
+ program.name("framer-code-link").description("Sync Framer code components to your local filesystem").version(version).argument("[projectHash]", "Framer Project ID Hash (auto-detected from package.json if omitted)").option("-n, --name <name>", "Project name (optional)").option("-d, --dir <directory>", "Explicit project directory").option("--once", "Exit after the initial sync completes").option("-v, --verbose", "Enable verbose logging").option("--log-level <level>", "Set log level (debug, info, warn, error)").option("--dangerously-auto-delete", "Automatically delete remote files without confirmation").addOption(new Option("--unsupported-npm [mode]", "Handle unsupported npm packages (default without mode: acquire-types; modes: acquire-types, package-manager)").argParser(parseUnsupportedNpmMode).preset("acquire-types")).action(async (projectHash, options) => {
4091
3993
  if (!projectHash) {
4092
3994
  const detected = await getProjectHashFromCwd();
4093
3995
  if (detected) projectHash = detected;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.21.0-alpha.2",
3
+ "version": "0.21.0-alpha.4",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",
@@ -26,6 +26,7 @@
26
26
  "@typescript/ata": "^0.9.8",
27
27
  "chokidar": "^5.0.0",
28
28
  "commander": "^14.0.3",
29
+ "parse-imports": "^3.0.0",
29
30
  "prettier": "^3.7.4",
30
31
  "typescript": "^5.9.3",
31
32
  "ws": "^8.18.3"