unframer 0.6.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/README.md ADDED
@@ -0,0 +1,138 @@
1
+ <div align='center'>
2
+ <br/>
3
+ <br/>
4
+ <h3>unframer</h3>
5
+ <br/>
6
+ <br/>
7
+ </div>
8
+
9
+ > [!IMPORTANT]
10
+ > If your component has any problem like missing imports create an issue with the component url, these issues are usually because of updates to the `framer` runtime which is kept in sync in this repository in [unframer/framer-fixed/dist](./unframer/framer-fixed/dist) folder
11
+
12
+ Download framer components as simple files
13
+
14
+ - Works with any React framework (Next.js, Gatsby, Vite, etc)
15
+ - Includes all your components dependencies
16
+ - Has Typescript support, inferred from your component variables (like `variant`)
17
+
18
+ ## Usage
19
+
20
+ 1. Install the package
21
+
22
+ ```sh
23
+ npm install unframer framer-motion
24
+ ```
25
+
26
+ 1. Create an `unframer.json` file like the following (the key will be used for the component folder inside `outDir`)
27
+
28
+ ```json
29
+ {
30
+ "outDir": "./framer",
31
+ "components": {
32
+ "logos": "https://framer.com/m/Logo-Ticker-1CEq.js@YtVlixDzOkypVBs3Dpav",
33
+ "menus": "https://framer.com/m/Mega-Menu-2wT3.js@W0zNsrcZ2WAwVuzt0BCl"
34
+ }
35
+ }
36
+ ```
37
+
38
+ 1. Copy your framer component url and add it to your config (remove the part after `@` to always use the latest version)
39
+
40
+ ![url import](./assets/framer-url-import.png)
41
+
42
+ 1. Run the command `npx unframer` to download the components and their types in the `outDir` directory
43
+ 1. Import the component inside your `jsx` files, for example
44
+
45
+ ```tsx
46
+ import Menu from './framer/menus'
47
+ import { FramerStyles } from 'unframer/dist/react'
48
+
49
+ export default function App() {
50
+ return (
51
+ <div>
52
+ {/* Injects fonts and other framer utility styles */}
53
+ <FramerStyles Components={[Menu]} />
54
+ <Menu componentVariable='some variable' />
55
+ </div>
56
+ )
57
+ }
58
+ ```
59
+
60
+ ## Using responsive variants
61
+
62
+ ```tsx
63
+ import Logos from './framer/logos'
64
+ import { FramerStyles } from 'unframer/dist/react'
65
+
66
+ export default function App() {
67
+ return (
68
+ <div>
69
+ {/* Injects fonts and other framer utility styles */}
70
+ <FramerStyles Components={[Logos]} />
71
+ {/* Changes component variant based on breakpoint */}
72
+ <Logos.Responsive
73
+ variants={{
74
+ Desktop: 'Logo Ticker',
75
+ Tablet: 'Logo Ticker - M',
76
+ Mobile: 'Logo Ticker - M',
77
+ }}
78
+ />
79
+ </div>
80
+ )
81
+ }
82
+ ```
83
+
84
+ ## Styling
85
+
86
+ You can use `className` or `style` props to style your components
87
+
88
+ Notice that you will often need to use `!important` to override styles already defined in framer like `width` and `height`
89
+
90
+ ```tsx
91
+ import Logos from './framer/logos'
92
+ import { FramerStyles } from 'unframer/dist/react'
93
+
94
+ export default function App() {
95
+ return (
96
+ <div>
97
+ {/* Injects fonts and other framer utility styles */}
98
+ <FramerStyles Components={[Logos]} />
99
+ {/* Changes component variant based on breakpoint */}
100
+ <Logos.responsive
101
+ className='!w-full'
102
+ variants={{
103
+ Desktop: 'Logo Ticker',
104
+ Tablet: 'Logo Ticker - M',
105
+ Mobile: 'Logo Ticker - M',
106
+ }}
107
+ />
108
+ </div>
109
+ )
110
+ }
111
+ ```
112
+
113
+ ## Supported component props
114
+
115
+ `unframer` will add TypeScript definitions for your Framer components props and variables, some example variables you can use are:
116
+
117
+ - `variant`, created when you use variants in Framer
118
+ - functions, created when you use an `event` variable in Framer
119
+ - Any scalar variable like String, Number, Boolean, Date, etc
120
+ - Image variables (object with `src`, `srcSet` and `alt`), created when you use an `image` variable in Framer
121
+ - Link strings, created when you make a link a variable in Framer
122
+ - Rich text, created when you use a `richText` variable in Framer
123
+ - Color, a string
124
+ - React component, created when you use a `component` variable in Framer, for example in the Ticker component
125
+
126
+ Known limitations:
127
+
128
+ - Color styles (also known as tokens) can get out of sync with your Framer project, if this happen you will have to find the corresponding css variable (in the form of `--token-xxxx`) in the component code and define it in your CSS, for example:
129
+
130
+ ```css
131
+ :root {
132
+ --token-64603892-5c8b-477a-82d6-e795e75dd5dc: #0b5c96;
133
+ }
134
+ ```
135
+
136
+ ## Example
137
+
138
+ Look at the [nextjs-app](./nextjs-app) folder for an example
package/bin.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { cli } from './dist/cli.js'
3
+
4
+ cli()
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function cli(): Promise<void>;
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.tsx"],"names":[],"mappings":"AAUA,wBAAsB,GAAG,kBA+CxB"}
package/dist/cli.js ADDED
@@ -0,0 +1,98 @@
1
+ import { bundle, logger } from './exporter.js';
2
+ import chokidar from 'chokidar';
3
+ import fs from 'fs-extra';
4
+ import { findUp } from 'find-up';
5
+ import path from 'path';
6
+ const configName = 'unframer.json';
7
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
8
+ export async function cli() {
9
+ const cwd = process.cwd();
10
+ const watch = process.argv.includes('--watch');
11
+ logger.log(`Looking for ${configName} in ${cwd}`);
12
+ const configPath = await findUp([configName], { cwd });
13
+ if (!configPath) {
14
+ logger.log(`No ${configName} found`);
15
+ return;
16
+ }
17
+ const configContent = fs.readFileSync(configPath, 'utf8');
18
+ if (!configContent) {
19
+ logger.log(`No ${configName} contents found`);
20
+ return;
21
+ }
22
+ let config = JSON.parse(configContent);
23
+ await processConfig(config);
24
+ if (watch) {
25
+ const watcher = chokidar.watch(configPath, {
26
+ persistent: true,
27
+ });
28
+ let controller = new AbortController();
29
+ watcher.on('change', async (path) => {
30
+ console.log();
31
+ controller.abort();
32
+ controller = new AbortController();
33
+ const newConfig = safeJsonParse(fs.readFileSync(configPath, 'utf8'));
34
+ if (!newConfig) {
35
+ logger.log(`Invalid ${configName} file`);
36
+ return;
37
+ }
38
+ const newNames = getNewNames(config, newConfig);
39
+ if (newNames.length) {
40
+ logger.log(`New components found: ${newNames.join(', ')}`);
41
+ await processConfig({
42
+ ...newConfig,
43
+ components: pluck(newConfig.components, newNames),
44
+ }, controller.signal);
45
+ }
46
+ config = newConfig;
47
+ });
48
+ }
49
+ }
50
+ function safeJsonParse(json) {
51
+ try {
52
+ return JSON.parse(json);
53
+ }
54
+ catch (e) {
55
+ return null;
56
+ }
57
+ }
58
+ function pluck(o, names) {
59
+ return Object.fromEntries(names.map((n) => [n, o[n]]));
60
+ }
61
+ function getNewNames(oldConfig, newConfig) {
62
+ // get the new names, also check if the previous url (object value) has changed
63
+ const oldKeys = Object.keys(oldConfig.components);
64
+ const newKeys = Object.keys(newConfig.components);
65
+ const newNames = newKeys.filter((key) => {
66
+ if (!oldKeys.includes(key)) {
67
+ return true;
68
+ }
69
+ if (oldConfig.components[key] !== newConfig.components[key]) {
70
+ return true;
71
+ }
72
+ return false;
73
+ });
74
+ return newNames;
75
+ }
76
+ async function processConfig(config, signal) {
77
+ try {
78
+ const { components, outDir } = config || {};
79
+ const installDir = path.resolve(process.cwd(), outDir || 'framer');
80
+ if (!components) {
81
+ logger.log(`No components found in ${configName}`);
82
+ return;
83
+ }
84
+ await bundle({
85
+ components,
86
+ cwd: installDir,
87
+ signal,
88
+ });
89
+ }
90
+ catch (e) {
91
+ if (signal) {
92
+ logger.log('Error processing config', e.message);
93
+ return;
94
+ }
95
+ throw e;
96
+ }
97
+ }
98
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,37 @@
1
+ import tmp from 'tmp';
2
+ import { test } from 'vitest';
3
+ import { bundle } from './exporter.js';
4
+ test('bundle simple component', async () => {
5
+ const tempFolder = tmp.dirSync({ unsafeCleanup: true }).name;
6
+ console.log('tempFolder', tempFolder);
7
+ const url = 'https://framer.com/m/Logo-Ticker-1CEq.js@YtVlixDzOkypVBs3Dpav';
8
+ await bundle({
9
+ components: {
10
+ ticker: url,
11
+ },
12
+ cwd: tempFolder,
13
+ });
14
+ }, 1000 * 10);
15
+ test('issue #1', async () => {
16
+ const tempFolder = tmp.dirSync({ unsafeCleanup: true }).name;
17
+ console.log('tempFolder', tempFolder);
18
+ const url = 'https://framer.com/m/Item-Qetw.js@vUDyI0yvPLONiBDf8Kzw';
19
+ await bundle({
20
+ components: {
21
+ item: url,
22
+ },
23
+ cwd: tempFolder,
24
+ });
25
+ }, 1000 * 10);
26
+ test('bundle ticker variant', async () => {
27
+ const tempFolder = tmp.dirSync({ unsafeCleanup: true }).name;
28
+ console.log('tempFolder', tempFolder);
29
+ const url = 'https://framer.com/m/Brand-Logo-Ticker-Uc8E.js@WLfLN2D3C6m9DWtZu0ci';
30
+ await bundle({
31
+ components: {
32
+ logos: url,
33
+ },
34
+ cwd: tempFolder,
35
+ });
36
+ }, 1000 * 10);
37
+ //# sourceMappingURL=cli.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,KAAK,CAAA;AAErB,OAAO,EAAE,IAAI,EAAU,MAAM,QAAQ,CAAA;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAEtC,IAAI,CACA,yBAAyB,EACzB,KAAK,IAAI,EAAE;IACP,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;IAC5D,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IACrC,MAAM,GAAG,GACL,+DAA+D,CAAA;IACnE,MAAM,MAAM,CAAC;QACT,UAAU,EAAE;YACR,MAAM,EAAE,GAAG;SACd;QACD,GAAG,EAAE,UAAU;KAClB,CAAC,CAAA;AACN,CAAC,EACD,IAAI,GAAG,EAAE,CACZ,CAAA;AACD,IAAI,CACA,UAAU,EACV,KAAK,IAAI,EAAE;IACP,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;IAC5D,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IACrC,MAAM,GAAG,GAAG,wDAAwD,CAAA;IACpE,MAAM,MAAM,CAAC;QACT,UAAU,EAAE;YACR,IAAI,EAAE,GAAG;SACZ;QACD,GAAG,EAAE,UAAU;KAClB,CAAC,CAAA;AACN,CAAC,EACD,IAAI,GAAG,EAAE,CACZ,CAAA;AACD,IAAI,CACA,uBAAuB,EACvB,KAAK,IAAI,EAAE;IACP,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;IAC5D,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,UAAU,CAAC,CAAA;IACrC,MAAM,GAAG,GACL,qEAAqE,CAAA;IACzE,MAAM,MAAM,CAAC;QACT,UAAU,EAAE;YACR,KAAK,EAAE,GAAG;SACb;QACD,GAAG,EAAE,UAAU;KAClB,CAAC,CAAA;AACN,CAAC,EACD,IAAI,GAAG,EAAE,CACZ,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { Plugin } from 'esbuild';
2
+ import { PropertyControls } from '../framer-fixed/dist/framer.js';
3
+ export declare const logger: {
4
+ log(...args: any[]): void;
5
+ error(...args: any[]): void;
6
+ };
7
+ export declare function bundle({ cwd: out, components, signal, }: {
8
+ cwd?: string | undefined;
9
+ components?: Record<string, string> | undefined;
10
+ signal?: AbortSignal | undefined;
11
+ }): Promise<void>;
12
+ export declare function extractPropControlsSafe(text: any, name: any): Promise<PropertyControls<any, any> | undefined>;
13
+ export declare function extractPropControlsUnsafe(filename: any, name: any): Promise<any>;
14
+ export declare function propControlsToType(controls?: PropertyControls): string;
15
+ export declare function parsePropertyControls(code: string): string | null;
16
+ export declare function esbuildPluginBundleDependencies({ signal, }: {
17
+ signal?: AbortSignal | undefined;
18
+ }): Plugin;
19
+ export declare function resolveRedirect(url?: string, redirectCache?: any): Promise<any>;
20
+ export declare function recursiveResolveRedirect(url?: string): Promise<string | undefined>;
21
+ //# sourceMappingURL=exporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAoB,MAAM,SAAS,CAAA;AAQlD,OAAO,EAGH,gBAAgB,EACnB,MAAM,gCAAgC,CAAA;AAQvC,eAAO,MAAM,MAAM;;;CAOlB,CAAA;AAYD,wBAAsB,MAAM,CAAC,EACzB,GAAG,EAAE,GAAQ,EACb,UAAyC,EACzC,MAAqC,GACxC;;;;CAAA,iBAsIA;AAMD,wBAAsB,uBAAuB,CAAC,IAAI,KAAA,EAAE,IAAI,KAAA,mDAmCvD;AAED,wBAAsB,yBAAyB,CAAC,QAAQ,KAAA,EAAE,IAAI,KAAA,gBA2B7D;AAWD,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,gBAAgB,UA8E7D;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,iBAqCjD;AAUD,wBAAgB,+BAA+B,CAAC,EAC5C,MAA6C,GAChD;;CAAA,UA6HA;AAED,wBAAsB,eAAe,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,GAAG,gBAYtE;AAED,wBAAsB,wBAAwB,CAAC,GAAG,CAAC,EAAE,MAAM,+BAa1D"}