Skip to content

Commit e20fba0

Browse files
authored
Merge pull request #1 from framer/init-ts
Initialise TypeScript for packages and have first complex example
2 parents 4031795 + 889b5e7 commit e20fba0

13 files changed

Lines changed: 637 additions & 230 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
EXAMPLE_PROJECT_URL=https://framer.com/projects/Sites--aabbccddeeff
2+
FRAMER_API_KEY=12345678-1234-1234-1234-123456789

.github/workflows/pull_request.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ jobs:
1717
- run: npm ci
1818
- run: npm run check
1919

20+
typecheck:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v6
24+
25+
- uses: actions/setup-node@v6
26+
with:
27+
node-version: 24
28+
cache: npm
29+
30+
- run: npm ci
31+
- run: npm run typecheck

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Framer Server API Examples
2+
3+
This repository contains examples for the Framer Server API. Each example is a standalone project that can be run independently.
4+
5+
## How to run examples
6+
7+
You need to obtain a Framer project URL and API key. You can get the API key from the Framer project settings and find the project URL in the browser URL bar.
8+
9+
Then, you need to set the `EXAMPLE_PROJECT_URL` and `FRAMER_API_KEY` environment variables.
10+
11+
## How to connect and get a framer client
12+
13+
```ts
14+
const projectUrl = "https://framer.com/projects/Sites--aabbccddeeff";
15+
16+
const framer = await connect(projectUrl, apiKey);
17+
// ... your code here ...
18+
await framer.disconnect();
19+
```
20+
21+
Starting with Node.js v24, you can use the `using` keyword to ensure that the Framer client is closed after the block is executed.
22+
23+
```ts
24+
using framer = await connect(projectUrl, apiKey);
25+
26+
// ... your code here ...
27+
// The disconnect is automatically called when the block is exited.
28+
```
29+
30+
You can also use the environment variable `FRAMER_API_KEY` to set the API key and omit the API key parameter.
31+
32+
```ts
33+
using framer = await connect(projectUrl);
34+
```

biome.json

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
3-
"vcs": {
4-
"enabled": true,
5-
"clientKind": "git",
6-
"useIgnoreFile": true
7-
},
8-
"files": {
9-
"ignoreUnknown": false
10-
},
11-
"formatter": {
12-
"enabled": true,
13-
"formatWithErrors": true,
14-
"indentStyle": "space",
15-
"indentWidth": 4,
16-
"lineWidth": 120,
17-
"lineEnding": "lf",
18-
"attributePosition": "auto",
2+
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
3+
"vcs": {
4+
"enabled": true,
5+
"clientKind": "git",
6+
"useIgnoreFile": true
7+
},
8+
"files": {
9+
"ignoreUnknown": false
10+
},
11+
"formatter": {
12+
"enabled": true,
13+
"formatWithErrors": true,
14+
"indentStyle": "space",
15+
"indentWidth": 4,
16+
"lineWidth": 120,
17+
"lineEnding": "lf",
18+
"attributePosition": "auto",
1919
"bracketSpacing": true
20-
},
21-
"linter": {
22-
"enabled": true,
23-
"rules": {
24-
"recommended": true
25-
}
26-
},
27-
"javascript": {
28-
"formatter": {
29-
"quoteStyle": "double"
30-
}
31-
},
32-
"assist": {
33-
"enabled": true,
34-
"actions": {
35-
"source": {
36-
"organizeImports": "on"
37-
}
38-
}
39-
}
20+
},
21+
"linter": {
22+
"enabled": true,
23+
"rules": {
24+
"recommended": true,
25+
"complexity": {
26+
"useLiteralKeys": "off"
27+
}
28+
}
29+
},
30+
"javascript": {
31+
"formatter": {
32+
"quoteStyle": "double"
33+
}
34+
},
35+
"assist": {
36+
"enabled": true,
37+
"actions": {
38+
"source": {
39+
"organizeImports": "on"
40+
}
41+
}
42+
}
4043
}

examples/csv-importer/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# CSV Importer
2+
3+
This example shows how to import a CSV file into a Framer collection.
4+
5+
How to use:
6+
7+
```bash
8+
node --env-file=../../.env src/csv-to-collection.ts
9+
10+
bun run src/csv-to-collection.ts
11+
12+
deno run src/csv-to-collection.ts
13+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
slug,title,description,price,inStock,category
2+
wireless-mouse,Wireless Mouse,Ergonomic wireless mouse with precision tracking,29.99,true,Electronics
3+
mechanical-keyboard,Mechanical Keyboard,RGB mechanical keyboard with cherry switches,89.99,true,Electronics
4+
usb-c-cable,USB-C Cable,Fast charging USB-C cable 6ft length,12.99,false,Accessories
5+
monitor-stand,Monitor Stand,Adjustable aluminum monitor stand,49.99,true,Furniture
6+
desk-lamp,LED Desk Lamp,Dimmable LED lamp with USB charging port,34.99,true,Lighting
7+
webcam-hd,HD Webcam,1080p webcam with built-in microphone,59.99,true,Electronics
8+
mouse-pad,Large Mouse Pad,Extended gaming mouse pad with stitched edges,19.99,true,Accessories
9+
headphone-stand,Headphone Stand,Wooden headphone stand with cable holder,24.99,false,Furniture

examples/csv-importer/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "csv-importer",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"typecheck": "tsc --noEmit"
8+
},
9+
"dependencies": {
10+
"framer-api": "^0.0.1-alpha.6",
11+
"papaparse": "^5.5.3",
12+
"typescript": "^5.9.3"
13+
},
14+
"devDependencies": {
15+
"@types/node": "^22.10.2",
16+
"@types/papaparse": "^5.3.15"
17+
}
18+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import assert from "node:assert";
2+
import path from "node:path";
3+
import { type CreateField, connect, type FieldDataEntryInput, type FieldDataInput } from "framer-api";
4+
import { type FieldType, loadCsv } from "./load-csv.ts";
5+
6+
// Configuration
7+
8+
const projectUrl = process.env["EXAMPLE_PROJECT_URL"];
9+
assert(projectUrl, "EXAMPLE_PROJECT_URL environment variable is required");
10+
11+
const csvPath = process.env["CSV_PATH"] ?? path.join(import.meta.dirname, "../data/sample-products.csv");
12+
const collectionName = process.env["COLLECTION_NAME"] ?? "Products";
13+
14+
const { columns, rows, fieldTypes } = loadCsv(csvPath);
15+
16+
assert(columns.includes("slug"), "CSV must contain a 'slug' column");
17+
18+
// The `using` keyword is used to ensure that the Framer client is closed after the block is executed.
19+
// If you don't use the `using` keyword, you need to manually close the client using `await framer.disconnect()`.
20+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/using
21+
using framer = await connect(projectUrl);
22+
23+
// Find or Create Collection
24+
25+
const existingCollections = await framer.getCollections();
26+
let collection = existingCollections.find((c) => c.name === collectionName);
27+
28+
if (!collection) {
29+
collection = await framer.createCollection(collectionName);
30+
}
31+
32+
// Add Missing Fields
33+
34+
const existingFields = await collection.getFields();
35+
const existingFieldNames = new Set(existingFields.map((f) => f.name.toLowerCase()));
36+
37+
const fieldsToCreate = columns
38+
.filter((column) => column !== "slug" && !existingFieldNames.has(column.toLowerCase()))
39+
.map(
40+
(column): CreateField => ({
41+
type: fieldTypes.get(column) ?? "string",
42+
name: column,
43+
}),
44+
);
45+
46+
if (fieldsToCreate.length > 0) {
47+
await collection.addFields(fieldsToCreate);
48+
}
49+
50+
// Build Items & Import
51+
52+
const fields = await collection.getFields();
53+
const fieldNameToId = new Map(fields.map((f) => [f.name.toLowerCase(), f.id]));
54+
55+
const existingItems = await collection.getItems();
56+
const slugToExistingId = new Map(existingItems.map((item) => [item.slug, item.id]));
57+
58+
const items = rows.map((row) => {
59+
const fieldData: FieldDataInput = {};
60+
61+
for (const column of columns) {
62+
if (column === "slug") continue;
63+
64+
const fieldId = fieldNameToId.get(column.toLowerCase());
65+
if (!fieldId) continue;
66+
67+
const value = row[column] ?? "";
68+
const fieldType = fieldTypes.get(column) ?? "string";
69+
fieldData[fieldId] = toFieldData(value, fieldType);
70+
}
71+
72+
const slug = row["slug"];
73+
assert(slug && slug.length > 0, "slug is required and must be non-empty");
74+
const existingId = slugToExistingId.get(slug);
75+
76+
return { id: existingId, slug, fieldData };
77+
});
78+
79+
await collection.addItems(items);
80+
81+
console.log(`Imported ${items.length} items`);
82+
83+
function toFieldData(value: string, type: FieldType): FieldDataEntryInput {
84+
switch (type) {
85+
case "boolean":
86+
return { type: "boolean" as const, value: value.toLowerCase() === "true" };
87+
case "number":
88+
return { type: "number" as const, value: parseFloat(value) || 0 };
89+
case "string":
90+
return { type: "string" as const, value };
91+
}
92+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { readFileSync } from "node:fs";
2+
import Papa from "papaparse";
3+
4+
export type FieldType = "string" | "number" | "boolean";
5+
6+
export interface CsvData {
7+
columns: string[];
8+
rows: Record<string, string>[];
9+
fieldTypes: Map<string, FieldType>;
10+
}
11+
12+
export function loadCsv(path: string): CsvData {
13+
const csvContent = readFileSync(path, "utf-8");
14+
const { data: rows, meta } = Papa.parse<Record<string, string>>(csvContent, {
15+
header: true,
16+
skipEmptyLines: true,
17+
transformHeader: (header: string) => header.trim(),
18+
transform: (value: string) => value.trim(),
19+
});
20+
21+
if (!meta.fields) {
22+
throw new Error("CSV file has no header row");
23+
}
24+
25+
const fieldTypes = new Map(inferFieldTypes(rows, meta.fields));
26+
27+
return { columns: meta.fields, rows, fieldTypes };
28+
}
29+
30+
function inferFieldType(values: string[]): FieldType {
31+
const nonEmptyValues = values.filter((v) => v !== "");
32+
if (nonEmptyValues.length === 0) return "string";
33+
34+
const allBooleans = nonEmptyValues.every((v) => v === "true" || v === "false");
35+
if (allBooleans) return "boolean";
36+
37+
const allNumbers = nonEmptyValues.every((v) => !Number.isNaN(parseFloat(v)) && Number.isFinite(Number(v)));
38+
if (allNumbers) return "number";
39+
40+
return "string";
41+
}
42+
43+
/**
44+
* Infer the field types from the data in the CSV file.
45+
* Returns the column name and the inferred field type.
46+
*/
47+
function inferFieldTypes(rows: Record<string, string>[], columns: string[]): [string, FieldType][] {
48+
return columns.map((column) => {
49+
const values = rows.map((row) => row[column] ?? "");
50+
return [column, inferFieldType(values)];
51+
});
52+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"include": ["**/*.ts"]
4+
}

0 commit comments

Comments
 (0)