framer-code-link 0.6.0 → 0.8.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.
package/dist/index.mjs CHANGED
@@ -7,9 +7,10 @@ import path from "path";
7
7
  import { createHash } from "crypto";
8
8
  import { setupTypeAcquisition } from "@typescript/ata";
9
9
  import ts from "typescript";
10
+ import { fileURLToPath } from "node:url";
10
11
  import chokidar from "chokidar";
11
12
 
12
- //#region rolldown:runtime
13
+ //#region \0rolldown/runtime.js
13
14
  var __create = Object.create;
14
15
  var __defProp = Object.defineProperty;
15
16
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1042,6 +1043,113 @@ function extractPackageFromUrl(url) {
1042
1043
  return /\/(@?[^@/]+(?:\/[^@/]+)?)/.exec(url)?.[1] ?? null;
1043
1044
  }
1044
1045
 
1046
+ //#endregion
1047
+ //#region src/helpers/skills.ts
1048
+ /**
1049
+ * Agent Skills installer — copies the skill into the project directory
1050
+ * and symlinks it into agent-specific paths.
1051
+ */
1052
+ /** Agent-specific skill directories that get symlinked to the canonical .skills/ location */
1053
+ const AGENT_SKILL_DIRS = [
1054
+ ".agents/skills",
1055
+ ".claude/skills",
1056
+ ".cursor/skills"
1057
+ ];
1058
+ /**
1059
+ * Read the skill name from the SKILL.md frontmatter.
1060
+ */
1061
+ async function readSkillName(sourceDir) {
1062
+ const content = await fs.readFile(path.join(sourceDir, "SKILL.md"), "utf-8");
1063
+ const match = /^name:\s*(.+)$/m.exec(content);
1064
+ if (!match) throw new Error("Could not read skill name from SKILL.md frontmatter");
1065
+ return match[1].trim();
1066
+ }
1067
+ /**
1068
+ * Recursively collect all file paths relative to a directory.
1069
+ */
1070
+ async function collectFiles(dir, base = "") {
1071
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1072
+ const files = [];
1073
+ for (const entry of entries) {
1074
+ const rel = base ? `${base}/${entry.name}` : entry.name;
1075
+ if (entry.isDirectory()) files.push(...await collectFiles(path.join(dir, entry.name), rel));
1076
+ else files.push(rel);
1077
+ }
1078
+ return files;
1079
+ }
1080
+ /**
1081
+ * Install the agent skill into the project.
1082
+ * Writes the canonical skill to .skills/<name>/ and symlinks
1083
+ * into agent-specific directories.
1084
+ */
1085
+ async function installSkills(projectDir) {
1086
+ const sourceDir = await findSkillsSourceDir();
1087
+ if (!sourceDir) {
1088
+ debug("Could not locate skills source files, skipping skill installation");
1089
+ return;
1090
+ }
1091
+ const skillName = await readSkillName(sourceDir);
1092
+ const canonicalDir = path.join(projectDir, ".skills", skillName);
1093
+ const skillMdPath = path.join(canonicalDir, "SKILL.md");
1094
+ try {
1095
+ await fs.access(skillMdPath);
1096
+ debug("Agent skills already installed");
1097
+ return;
1098
+ } catch {}
1099
+ const files = await collectFiles(sourceDir);
1100
+ for (const file of files) {
1101
+ const src = path.join(sourceDir, file);
1102
+ const dest = path.join(canonicalDir, file);
1103
+ try {
1104
+ await fs.mkdir(path.dirname(dest), { recursive: true });
1105
+ await fs.copyFile(src, dest);
1106
+ } catch (err) {
1107
+ debug(`Failed to copy skill file ${file}`, err);
1108
+ return;
1109
+ }
1110
+ }
1111
+ debug(`Installed agent skill to .skills/${skillName}/`);
1112
+ for (const agentDir of AGENT_SKILL_DIRS) {
1113
+ const linkDir = path.join(projectDir, agentDir);
1114
+ const linkPath = path.join(linkDir, skillName);
1115
+ const relativeTarget = path.relative(linkDir, canonicalDir);
1116
+ try {
1117
+ await fs.mkdir(linkDir, { recursive: true });
1118
+ try {
1119
+ const stat = await fs.lstat(linkPath);
1120
+ if (stat.isSymbolicLink() || stat.isDirectory()) await fs.rm(linkPath, { recursive: true });
1121
+ } catch {}
1122
+ await fs.symlink(relativeTarget, linkPath, "dir");
1123
+ debug(`Symlinked ${agentDir}/${skillName} -> ${relativeTarget}`);
1124
+ } catch (err) {
1125
+ debug(`Symlink failed for ${agentDir}, falling back to copy`, err);
1126
+ try {
1127
+ await fs.cp(canonicalDir, linkPath, { recursive: true });
1128
+ } catch {
1129
+ debug(`Copy fallback also failed for ${agentDir}`);
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Find the skills source directory shipped with the package.
1136
+ * Walks up from the current file to find the package root, then resolves skills/.
1137
+ */
1138
+ async function findSkillsSourceDir() {
1139
+ let dir = path.dirname(fileURLToPath(import.meta.url));
1140
+ for (let i = 0; i < 10; i++) try {
1141
+ await fs.access(path.join(dir, "package.json"));
1142
+ const candidate = path.join(dir, "skills");
1143
+ await fs.access(path.join(candidate, "SKILL.md"));
1144
+ return candidate;
1145
+ } catch {
1146
+ const parent = path.dirname(dir);
1147
+ if (parent === dir) break;
1148
+ dir = parent;
1149
+ }
1150
+ return null;
1151
+ }
1152
+
1045
1153
  //#endregion
1046
1154
  //#region src/helpers/installer.ts
1047
1155
  /**
@@ -1153,7 +1261,8 @@ var Installer = class {
1153
1261
  this.ensureTsConfig(),
1154
1262
  this.ensurePrettierConfig(),
1155
1263
  this.ensureFramerDeclarations(),
1156
- this.ensurePackageJson()
1264
+ this.ensurePackageJson(),
1265
+ this.ensureSkills()
1157
1266
  ]);
1158
1267
  Promise.resolve().then(async () => {
1159
1268
  await this.ensureReact18Types();
@@ -1304,6 +1413,9 @@ declare module "*.json"
1304
1413
  debug("Created package.json");
1305
1414
  }
1306
1415
  }
1416
+ async ensureSkills() {
1417
+ await installSkills(this.projectDir);
1418
+ }
1307
1419
  async ensureReact18Types() {
1308
1420
  const reactTypesDir = path.join(this.projectDir, "node_modules/@types/react");
1309
1421
  const reactFiles = [
@@ -1797,7 +1909,6 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
1797
1909
  version: "1.0.0",
1798
1910
  private: true,
1799
1911
  shortProjectHash: shortId,
1800
- framerProjectHash: projectHash,
1801
1912
  framerProjectName: projectName
1802
1913
  };
1803
1914
  await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2));
@@ -1990,12 +2101,9 @@ function transition(state, event) {
1990
2101
  case "REMOTE_FILE_CHANGE": {
1991
2102
  const validation = validateIncomingChange(event.fileMeta, state.mode);
1992
2103
  if (validation.action === "queue") {
1993
- effects.push(log("debug", `Queueing file change: ${event.file.name} (${validation.reason})`));
2104
+ effects.push(log("debug", `Ignoring file change during sync: ${event.file.name}`));
1994
2105
  return {
1995
- state: {
1996
- ...state,
1997
- pendingRemoteChanges: [...state.pendingRemoteChanges, event.file]
1998
- },
2106
+ state,
1999
2107
  effects
2000
2108
  };
2001
2109
  }
@@ -2413,9 +2521,7 @@ async function start(config) {
2413
2521
  let syncState = {
2414
2522
  mode: "disconnected",
2415
2523
  socket: null,
2416
- pendingRemoteChanges: [],
2417
- pendingOperations: /* @__PURE__ */ new Map(),
2418
- nextOperationId: 1
2524
+ pendingRemoteChanges: []
2419
2525
  };
2420
2526
  const userActions = new PluginUserPromptCoordinator();
2421
2527
  async function processEvent(event) {
@@ -2518,7 +2624,7 @@ async function start(config) {
2518
2624
  await processEvent({
2519
2625
  type: "LOCAL_DELETE_REJECTED",
2520
2626
  fileName: file.fileName,
2521
- content: file.content ?? ""
2627
+ content: file.content
2522
2628
  });
2523
2629
  }
2524
2630
  return;
@@ -2612,7 +2718,7 @@ program.name("framer-code-link").description("Sync Framer code components to you
2612
2718
  const detected = await getProjectHashFromCwd();
2613
2719
  if (detected) projectHash = detected;
2614
2720
  else {
2615
- console.error("No project ID provided and no existing code-link directory found.");
2721
+ console.error("No Project ID provided and no existing Code Link directory found.");
2616
2722
  console.error("Copy the command from the Code Link Plugin to get started.");
2617
2723
  process.exit(1);
2618
2724
  }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "framer-code-link",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI tool for syncing Framer code components - controller-centric architecture",
5
5
  "main": "dist/index.mjs",
6
6
  "type": "module",
7
7
  "bin": "./dist/index.mjs",
8
8
  "files": [
9
- "dist"
9
+ "dist",
10
+ "skills"
10
11
  ],
11
12
  "scripts": {
12
13
  "dev": "NODE_ENV=development tsx src/index.ts",
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: framer-component-best-practices
3
+ description: Best practices for building and improving React code components in Framer, a no-code website builder. Covers property controls, animations, accessibility, and platform constraints. Use when creating, editing, or reviewing Framer components, working with ControlType property controls, or building React components for Framer projects.
4
+ license: MIT
5
+ metadata:
6
+ author: framer
7
+ version: "1.0"
8
+ ---
9
+
10
+ # Framer Component Best Practices
11
+
12
+ Best practices for building and improving React components in Framer with property controls, animations, and accessibility.
13
+
14
+ ## Core Rules
15
+
16
+ ### Component Structure
17
+
18
+ ```tsx
19
+ import { addPropertyControls, ControlType } from "framer";
20
+ import { motion } from "framer-motion"; // NOT from "framer"
21
+
22
+ interface MyComponentProps {
23
+ /* typed props */
24
+ }
25
+
26
+ /**
27
+ * @framerSupportedLayoutWidth any-prefer-fixed
28
+ * @framerSupportedLayoutHeight any-prefer-fixed
29
+ */
30
+ export default function MyComponent(props: MyComponentProps) {
31
+ // component
32
+ }
33
+
34
+ addPropertyControls(MyComponent, {
35
+ /* controls */
36
+ });
37
+ ```
38
+
39
+ ### Platform Constraints
40
+
41
+ These will cause errors if violated:
42
+
43
+ 1. **Single file, default export** - Use named `function` syntax (not arrow functions), no named exports
44
+ 2. **Imports** - Only `react`, `react-dom`, `framer`, `framer-motion`. Import `motion` from `"framer-motion"`, not `"framer"`
45
+ 3. **Position** - Use `position: relative` on the root element, never `fixed`
46
+ 4. **SSR** - Guard `window`/`document` access: `if (typeof window !== "undefined")`
47
+ 5. **Annotations** - Include `@framerSupportedLayoutWidth/Height` in a `/** */` block comment immediately above the component function
48
+ 6. **Types** - Provide a typed props interface (e.g. `MyComponentProps`). Avoid NodeJS types like `Timeout` — use `number` instead
49
+
50
+ ### Layout Annotations
51
+
52
+ | Content | Width | Height |
53
+ | ----------------- | ------------------ | ------------------ |
54
+ | No intrinsic size | `fixed` | `fixed` |
55
+ | Text/auto-sizing | `auto` | `auto` |
56
+ | Flexible | `any-prefer-fixed` | `any-prefer-fixed` |
57
+
58
+ Detect auto vs fixed sizing: check if `style.width` or `style.height` is `"100%"`.
59
+
60
+ ### Property Controls
61
+
62
+ To make components configurable in Framer's properties panel, add property controls:
63
+
64
+ - To make colors customizable, use `ControlType.Color`. Reuse the same prop for elements sharing a color.
65
+ - To make text styling customizable, use `ControlType.Font` with `controls: "extended"` and `defaultFontType: "sans-serif"`.
66
+ - For images, use `ControlType.ResponsiveImage`. Set defaults in the component body via destructuring (the control doesn't support `defaultValue`).
67
+ - Provide a `defaultValue` for every prop so components render correctly in the Framer canvas. Include at least one item in `ControlType.Array` controls.
68
+ - `ComponentName.defaultProps` is not supported in Framer — use `defaultValue` on the property control instead.
69
+ - Use `hidden` for conditional visibility: `hidden: (props) => !props.showFeature`
70
+ - Prefer sliders over steppers unless step values are large.
71
+ - Keep controls focused — make key elements configurable, hardcode the rest.
72
+ - See [Property Control Guide](references/PROPERTY_CONTROL_GUIDE.md) for detailed patterns, font styling rules, and recommended default values.
73
+
74
+ ### Image Defaults (in component body)
75
+
76
+ ```tsx
77
+ const {
78
+ image = {
79
+ src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
80
+ alt: "Default",
81
+ },
82
+ } = props;
83
+ ```
84
+
85
+ ### Animation Performance
86
+
87
+ ```tsx
88
+ import { useIsStaticRenderer } from "framer";
89
+ import { useInView } from "framer-motion";
90
+
91
+ const isStatic = useIsStaticRenderer();
92
+ const ref = useRef(null);
93
+ const isInView = useInView(ref);
94
+
95
+ if (isStatic) return <StaticPreview />; // Show useful static state
96
+ // Pause animations when out of viewport
97
+ ```
98
+
99
+ - For very complex animations, consider WebGL instead of `framer-motion`.
100
+ - Static preview should include visual effects, not just text.
101
+ - Wrapping state updates in `startTransition()` prevents UI blocking and keeps interactions smooth.
102
+
103
+ ### Text
104
+
105
+ - For auto-sized components with text, apply `width: max-content` or `minWidth: max-content` to prevent text from collapsing.
106
+
107
+ ### Common Errors
108
+
109
+ - WebGL cross-origin: handle `SecurityError: Failed to execute 'texImage2D'` for cross-origin images.
110
+ - Inverted Y-axis: check if WebGL images render upside down and accommodate.
111
+
112
+ ### Accessibility
113
+
114
+ - `aria` roles on interactive elements
115
+ - Semantic HTML (`<nav>`, `<article>`, `<section>`)
116
+ - `alt=""` on decorative images
117
+ - 4.5:1 color contrast
118
+
119
+ ## Term Interpretation
120
+
121
+ - "responsive" → width/height 100%
122
+ - "modern" → 8px radius, 16px spacing, subtle shadows
123
+ - "minimal" → limited colors, whitespace
124
+ - "interactive" → hover/active states
125
+ - "accessible" → ARIA, semantic HTML
126
+ - "props"/"properties" → Framer property controls
127
+
128
+ ## Reference Files
129
+
130
+ - [Property Controls](references/PROPERTY_CONTROLS.md) - All ControlType documentation with examples
131
+ - [Property Control Types](references/PROPERTY_CONTROL_TYPES.md) - TypeScript interfaces for all control types
132
+ - [Property Control Guide](references/PROPERTY_CONTROL_GUIDE.md) - Font patterns, styling rules, and recommended default values
133
+ - [Example Components](references/EXAMPLES.md) - Cookie banner, image compare, sticky notes, twemoji