@reframejs/unframer 4.2.0-reframe.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 +389 -0
- package/bin.js +5 -0
- package/dist/babel-jsx.d.ts +24 -0
- package/dist/babel-jsx.js +299 -0
- package/dist/babel-plugin-imports.d.ts +15 -0
- package/dist/babel-plugin-imports.js +211 -0
- package/dist/babel-typedoc.d.ts +39 -0
- package/dist/babel-typedoc.js +76 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +821 -0
- package/dist/compat.d.ts +2 -0
- package/dist/compat.js +23 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +27 -0
- package/dist/css-core.d.ts +50 -0
- package/dist/css-core.js +231 -0
- package/dist/css.d.ts +15 -0
- package/dist/css.js +36 -0
- package/dist/esbuild.d.ts +31 -0
- package/dist/esbuild.js +317 -0
- package/dist/exporter.d.ts +96 -0
- package/dist/exporter.js +1151 -0
- package/dist/flat-cache-interceptor.d.ts +24 -0
- package/dist/flat-cache-interceptor.js +87 -0
- package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
- package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
- package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
- package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
- package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
- package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
- package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
- package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
- package/dist/framer-client.server.d.ts +4 -0
- package/dist/framer-client.server.js +45 -0
- package/dist/framer-package-versions.d.ts +781 -0
- package/dist/framer-package-versions.js +782 -0
- package/dist/framer-types.d.ts +30 -0
- package/dist/framer-types.js +35 -0
- package/dist/framer.d.ts +4990 -0
- package/dist/framer.js +64296 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/package-manager.d.ts +10 -0
- package/dist/package-manager.js +138 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
- package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/cms.js +58 -0
- package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/errors.js +48 -0
- package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +3 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.js +46 -0
- package/dist/plugin-mcp-dist/lib/framer.d.ts +30 -0
- package/dist/plugin-mcp-dist/lib/framer.js +1084 -0
- package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +142 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +385 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1924 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +170 -0
- package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
- package/dist/plugin-mcp-dist/lib/react-export.js +358 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts +276 -0
- package/dist/plugin-mcp-dist/lib/schema.js +907 -0
- package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/store.js +10 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
- package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
- package/dist/plugin-mcp-dist/lib/types.js +188 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/utils.js +57 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
- package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/xml.js +395 -0
- package/dist/react.d.ts +76 -0
- package/dist/react.js +280 -0
- package/dist/renamer.d.ts +11 -0
- package/dist/renamer.js +139 -0
- package/dist/sentry.d.ts +2 -0
- package/dist/sentry.js +5 -0
- package/dist/stackblitz.d.ts +9 -0
- package/dist/stackblitz.js +179 -0
- package/dist/styles/framer.css +920 -0
- package/dist/styles/reset.css +37 -0
- package/dist/typescript.d.ts +24 -0
- package/dist/typescript.js +197 -0
- package/dist/undici-dispatcher.d.ts +2 -0
- package/dist/undici-dispatcher.js +10 -0
- package/dist/unframer-loader.d.ts +2 -0
- package/dist/unframer-loader.js +12 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +81 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +131 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { setMaxListeners } from 'events';
|
|
3
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
4
|
+
import pico from 'picocolors';
|
|
5
|
+
const { blue, bgBlue, green } = pico;
|
|
6
|
+
import { fetch } from 'undici';
|
|
7
|
+
import './sentry.js';
|
|
8
|
+
import { input, select, password } from '@inquirer/prompts';
|
|
9
|
+
import { bundle, createExampleComponentCode } from './exporter.js';
|
|
10
|
+
import { generateStackblitzFiles } from './stackblitz.js';
|
|
11
|
+
import { goke, wrapJsonSchema } from 'goke';
|
|
12
|
+
import { exec } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path, { basename } from 'path';
|
|
16
|
+
import { defaultBreakpointSizes } from './css-core.js';
|
|
17
|
+
import { componentNameToPath, dedent, isTruthy, kebabCase, logger, sleep, spinner, } from './utils.js';
|
|
18
|
+
import { getPackageManager } from './package-manager.js';
|
|
19
|
+
import { notifyError } from './sentry.js';
|
|
20
|
+
import { dispatcher } from './undici-dispatcher.js';
|
|
21
|
+
import { loadConfig, saveConfig, getConfigPath, } from './config.js';
|
|
22
|
+
import { addMcpCommands } from '@goke/mcp';
|
|
23
|
+
const configNames = ['unframer.config.json', 'unframer.json'];
|
|
24
|
+
export const cli = goke('unframer');
|
|
25
|
+
let defaultOutDir = 'framer';
|
|
26
|
+
cli.command('[...projectIds]', 'Run unframer with one or more project IDs')
|
|
27
|
+
.option('--outDir <dir>', 'Output directory')
|
|
28
|
+
.option('--external [package]', 'Make some package external, do not pass a package name to make all packages external')
|
|
29
|
+
.option('--watch', 'Watch for changes and rebuild')
|
|
30
|
+
.option('--jsx', 'Output jsx code instead of minified .js code')
|
|
31
|
+
.option('--debug', 'Enable debug logging')
|
|
32
|
+
.option('--metafile', 'Generate meta.json file with build metadata')
|
|
33
|
+
.action(async function main(projectIds, options) {
|
|
34
|
+
const outDir = options.outDir || defaultOutDir;
|
|
35
|
+
const jsx = options.jsx ?? true;
|
|
36
|
+
const external_ = options.external;
|
|
37
|
+
// --external without value gives "" (optional value); treat same as undefined (all external)
|
|
38
|
+
const allExternal = !external_ || external_ === '';
|
|
39
|
+
const externalPackages = external_ && external_ !== ''
|
|
40
|
+
? [external_]
|
|
41
|
+
: [];
|
|
42
|
+
try {
|
|
43
|
+
if (options.debug) {
|
|
44
|
+
logger.debug = true;
|
|
45
|
+
}
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
const signal = controller.signal;
|
|
48
|
+
const watch = options.watch;
|
|
49
|
+
if (projectIds.length > 0) {
|
|
50
|
+
if (watch && projectIds.length > 1) {
|
|
51
|
+
console.error('--watch is only supported with a single project ID');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Fetch configs for all project IDs
|
|
55
|
+
const fetchResults = await Promise.all(projectIds.map((projectId) => {
|
|
56
|
+
return configFromFetch({
|
|
57
|
+
allExternal,
|
|
58
|
+
externalPackages,
|
|
59
|
+
outDir,
|
|
60
|
+
projectId,
|
|
61
|
+
});
|
|
62
|
+
}));
|
|
63
|
+
// Merge configs when multiple projects are provided, otherwise use single config directly
|
|
64
|
+
const mergedConfig = fetchResults.length === 1
|
|
65
|
+
? fetchResults[0].config
|
|
66
|
+
: mergeConfigs(fetchResults.map((r) => r.config));
|
|
67
|
+
const cwd = fetchResults[0].cwd;
|
|
68
|
+
const websiteUrl = fetchResults[0].websiteUrl;
|
|
69
|
+
const { rebuild, buildContext } = await bundle({
|
|
70
|
+
config: {
|
|
71
|
+
jsx,
|
|
72
|
+
...mergedConfig,
|
|
73
|
+
},
|
|
74
|
+
watch,
|
|
75
|
+
cwd,
|
|
76
|
+
signal,
|
|
77
|
+
metafile: options.metafile,
|
|
78
|
+
});
|
|
79
|
+
// console.log('buildContext', buildContext)
|
|
80
|
+
if (!websiteUrl || !options.watch) {
|
|
81
|
+
await buildContext?.dispose?.();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
spinner.start(`Waiting for changes, try editing a component in Framer and click publish...`);
|
|
85
|
+
let lastEtag = null;
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
while (Date.now() - startTime < 30 * 60 * 1000) {
|
|
88
|
+
const etag = await fetch(websiteUrl, {
|
|
89
|
+
method: 'HEAD',
|
|
90
|
+
dispatcher,
|
|
91
|
+
})
|
|
92
|
+
.then((response) => response.headers.get('etag'))
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
logger.error('Error fetching etag:', error);
|
|
95
|
+
return null;
|
|
96
|
+
});
|
|
97
|
+
logger.log('etag', etag);
|
|
98
|
+
if (etag && lastEtag && etag !== lastEtag) {
|
|
99
|
+
spinner.start(`Detected Framer website change, rebuilding...`);
|
|
100
|
+
lastEtag = etag;
|
|
101
|
+
await rebuild();
|
|
102
|
+
}
|
|
103
|
+
if (etag) {
|
|
104
|
+
lastEtag = etag;
|
|
105
|
+
}
|
|
106
|
+
await sleep(1000 * 2);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// legacy behavior without Framer plugin
|
|
110
|
+
fixOldUnframerPath();
|
|
111
|
+
const cwd = process.cwd();
|
|
112
|
+
logger.log(`Looking for ${configNames.join(', ')} in ${cwd}`);
|
|
113
|
+
const configPath = findUp(configNames, { cwd });
|
|
114
|
+
if (!configPath) {
|
|
115
|
+
logger.log(`No ${configNames.join(', ')} found`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const configBasename = basename(configPath);
|
|
119
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
120
|
+
if (!configContent) {
|
|
121
|
+
logger.log(`No ${configBasename} contents found`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const configContentWithoutComments = configContent.replace(/^\s*\/\/.*$/gm, '');
|
|
125
|
+
const config = JSON.parse(configContentWithoutComments);
|
|
126
|
+
if (outDir !== defaultOutDir) {
|
|
127
|
+
config.outDir = outDir;
|
|
128
|
+
}
|
|
129
|
+
setMaxListeners(0, controller.signal);
|
|
130
|
+
const { buildContext } = await bundle({
|
|
131
|
+
config: { ...config, externalPackages, allExternal },
|
|
132
|
+
watch,
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
cwd: path.resolve(process.cwd(), config.outDir || 'framer'),
|
|
135
|
+
metafile: options.metafile,
|
|
136
|
+
});
|
|
137
|
+
await buildContext.dispose?.();
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
notifyError(error);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
function fixOldUnframerPath() {
|
|
145
|
+
// if unframer.json exists, rename it to unframer.config.json
|
|
146
|
+
const oldConfigPath = fs.existsSync('unframer.json');
|
|
147
|
+
if (oldConfigPath) {
|
|
148
|
+
fs.renameSync('unframer.json', 'unframer.config.json');
|
|
149
|
+
logger.green('legacy unframer.json config renamed to unframer.config.json');
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const version = pkg.version;
|
|
155
|
+
cli.version(version).help();
|
|
156
|
+
cli.command('example-app <projectId>', 'Create an example app with Framer components')
|
|
157
|
+
.option('--outDir <dir>', 'Output directory')
|
|
158
|
+
.action(async (projectId, options) => {
|
|
159
|
+
if (!projectId?.trim()) {
|
|
160
|
+
console.log(`unframer example-app requires a project id positional param`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const outDir = options.outDir || 'example-unframer-app';
|
|
165
|
+
console.log(`Creating example app in ${outDir}`);
|
|
166
|
+
// Create the output directory
|
|
167
|
+
const absoluteOutDir = path.resolve(process.cwd(), outDir);
|
|
168
|
+
if (!fs.existsSync(absoluteOutDir)) {
|
|
169
|
+
fs.mkdirSync(absoluteOutDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
// Fetch the project configuration
|
|
172
|
+
spinner.start('Fetching project configuration...');
|
|
173
|
+
const { config, websiteUrl } = await configFromFetch({
|
|
174
|
+
projectId,
|
|
175
|
+
allExternal: true,
|
|
176
|
+
externalPackages: [],
|
|
177
|
+
outDir: 'src/framer',
|
|
178
|
+
});
|
|
179
|
+
spinner.stop('Project configuration fetched');
|
|
180
|
+
// Generate example component code
|
|
181
|
+
spinner.start('Generating example component code...');
|
|
182
|
+
const { exampleCode } = await createExampleComponentCode({
|
|
183
|
+
outDir: './framer',
|
|
184
|
+
config,
|
|
185
|
+
});
|
|
186
|
+
// Generate all the files needed for the example app
|
|
187
|
+
const files = generateStackblitzFiles({
|
|
188
|
+
projectId,
|
|
189
|
+
appComponentCode: exampleCode,
|
|
190
|
+
title: config.projectName || 'Unframer Example App',
|
|
191
|
+
});
|
|
192
|
+
// Write all files to the output directory
|
|
193
|
+
for (const file of files) {
|
|
194
|
+
const filePath = path.join(absoluteOutDir, file.relativePath);
|
|
195
|
+
const fileDir = path.dirname(filePath);
|
|
196
|
+
// Ensure directory exists
|
|
197
|
+
if (!fs.existsSync(fileDir)) {
|
|
198
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
fs.writeFileSync(filePath, file.contents);
|
|
201
|
+
console.log(`Created ${file.relativePath}`);
|
|
202
|
+
}
|
|
203
|
+
spinner.stop('Example files created');
|
|
204
|
+
// Bundle the Framer components
|
|
205
|
+
spinner.start('Downloading Framer components...');
|
|
206
|
+
const componentsOutDir = path.join(absoluteOutDir, 'src/framer');
|
|
207
|
+
const { buildContext } = await bundle({
|
|
208
|
+
config: {
|
|
209
|
+
...config,
|
|
210
|
+
jsx: true,
|
|
211
|
+
outDir: componentsOutDir,
|
|
212
|
+
allExternal: true,
|
|
213
|
+
externalPackages: [],
|
|
214
|
+
},
|
|
215
|
+
watch: false,
|
|
216
|
+
cwd: componentsOutDir,
|
|
217
|
+
signal: new AbortController().signal,
|
|
218
|
+
metafile: false,
|
|
219
|
+
});
|
|
220
|
+
await buildContext?.dispose?.();
|
|
221
|
+
spinner.stop('Framer components downloaded');
|
|
222
|
+
// Install dependencies using detected package manager
|
|
223
|
+
spinner.stop('Framer components downloaded');
|
|
224
|
+
const packageManager = await getPackageManager();
|
|
225
|
+
const installCommand = packageManager === 'yarn' ? 'yarn' : `${packageManager} install`;
|
|
226
|
+
console.log(`Installing dependencies with ${packageManager}...`);
|
|
227
|
+
spinner.start(`Running ${installCommand}...`);
|
|
228
|
+
const execAsync = promisify(exec);
|
|
229
|
+
try {
|
|
230
|
+
await execAsync(installCommand, {
|
|
231
|
+
cwd: absoluteOutDir,
|
|
232
|
+
// Can't use 'inherit' with async exec, so we'll capture output
|
|
233
|
+
encoding: 'utf8',
|
|
234
|
+
});
|
|
235
|
+
spinner.stop('Dependencies installed successfully');
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
spinner.stop('Failed to install dependencies');
|
|
239
|
+
console.error(`${packageManager} install failed:`, error?.message || error);
|
|
240
|
+
console.log(`You can manually run "${installCommand}" in the created directory`);
|
|
241
|
+
}
|
|
242
|
+
console.info(dedent `
|
|
243
|
+
|
|
244
|
+
Example app created successfully in ${outDir}!
|
|
245
|
+
|
|
246
|
+
Next steps:
|
|
247
|
+
cd ${outDir}
|
|
248
|
+
${packageManager} run dev
|
|
249
|
+
|
|
250
|
+
Quick guide:
|
|
251
|
+
▪︎ Read and edit src/App.tsx to add or customize your rendered Framer components
|
|
252
|
+
▪︎ Your components are in src/framer/ directory. Run npm run framer to sync changes from Framer
|
|
253
|
+
▪︎ The app uses Vite + React + TypeScript + Tailwind CSS
|
|
254
|
+
|
|
255
|
+
`);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
notifyError(error);
|
|
259
|
+
spinner.error('Failed to create example app');
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
cli.command('mcp login [url]', 'Login to Framer MCP. Choose between plugin mode (requires Framer open) or server API mode (works headlessly with API key).').action(async (url) => {
|
|
264
|
+
try {
|
|
265
|
+
// If URL is passed directly, use plugin mode
|
|
266
|
+
if (url) {
|
|
267
|
+
saveConfig({ mode: 'plugin', mcpUrl: url });
|
|
268
|
+
console.log(`MCP URL saved to ${getConfigPath()}`);
|
|
269
|
+
console.log(`Run \`unframer --help\` to see all available MCP commands`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Show mode selection dropdown
|
|
273
|
+
const mode = await select({
|
|
274
|
+
message: 'Select authentication mode:',
|
|
275
|
+
choices: [
|
|
276
|
+
{
|
|
277
|
+
value: 'plugin',
|
|
278
|
+
name: 'MCP Plugin (Recommended)',
|
|
279
|
+
description: 'Requires Framer to be open with MCP plugin running. Paste URL from plugin.',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
value: 'server-api',
|
|
283
|
+
name: 'Server API',
|
|
284
|
+
description: 'Works without Framer open. Requires API key from project settings.',
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
if (mode === 'plugin') {
|
|
289
|
+
const shortcut = process.platform === 'darwin' ? 'Cmd+K' : 'Ctrl+K';
|
|
290
|
+
console.log('\nTo get your MCP URL:');
|
|
291
|
+
console.log(' 1. Go to https://framer.com and open your project');
|
|
292
|
+
console.log(` 2. Press ${shortcut} and search for "MCP" plugin`);
|
|
293
|
+
console.log(' 3. Copy the URL shown in the plugin\n');
|
|
294
|
+
const mcpUrl = await input({ message: 'Paste MCP URL:' });
|
|
295
|
+
if (!mcpUrl) {
|
|
296
|
+
console.error('MCP URL is required');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
saveConfig({ mode: 'plugin', mcpUrl });
|
|
300
|
+
console.log(`\nMCP URL saved to ${getConfigPath()}`);
|
|
301
|
+
console.log(`Run \`unframer --help\` to see all available MCP commands`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// Server API mode
|
|
305
|
+
console.log('\nTo get your API key:');
|
|
306
|
+
console.log(' 1. Open your Framer project at https://framer.com/projects');
|
|
307
|
+
console.log(' 2. Go to Project Settings > API');
|
|
308
|
+
console.log(' 3. Generate or copy your API key\n');
|
|
309
|
+
const apiKey = await password({ message: 'Enter Framer API key:', mask: '*' });
|
|
310
|
+
if (!apiKey) {
|
|
311
|
+
console.error('API key is required');
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
console.log('\nTo get your project URL:');
|
|
315
|
+
console.log(' Copy the URL from your browser when viewing the project');
|
|
316
|
+
console.log(' Example: https://framer.com/projects/MyProject--abc123\n');
|
|
317
|
+
const projectUrl = await input({
|
|
318
|
+
message: 'Enter Framer project URL (optional, can use --project later):',
|
|
319
|
+
});
|
|
320
|
+
saveConfig({
|
|
321
|
+
mode: 'server-api',
|
|
322
|
+
framerApiKey: apiKey,
|
|
323
|
+
framerProjectUrl: projectUrl || undefined,
|
|
324
|
+
});
|
|
325
|
+
console.log(`\nServer API credentials saved to ${getConfigPath()}`);
|
|
326
|
+
console.log(`\nUsage:`);
|
|
327
|
+
if (projectUrl) {
|
|
328
|
+
console.log(` unframer mcp getProjectXml`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.log(` unframer mcp getProjectXml --project "https://framer.com/projects/..."`);
|
|
332
|
+
}
|
|
333
|
+
console.log(`\nOr set FRAMER_PROJECT_URL environment variable`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
// Add MCP tool commands
|
|
344
|
+
const config = loadConfig();
|
|
345
|
+
const cliArgs = process.argv.slice(2);
|
|
346
|
+
const hasMcpCommand = cliArgs.includes('mcp');
|
|
347
|
+
const hasProjectOption = cliArgs.some((arg) => {
|
|
348
|
+
return arg === '--project' || arg.startsWith('--project=');
|
|
349
|
+
});
|
|
350
|
+
const hasMcpEvalCommand = hasMcpCommand && cliArgs.includes('eval');
|
|
351
|
+
const hasServerApiEnv = Boolean(process.env.FRAMER_PROJECT_URL || process.env.FRAMER_API_KEY);
|
|
352
|
+
const shouldUseServerApi = hasMcpCommand && (hasProjectOption || hasMcpEvalCommand || hasServerApiEnv);
|
|
353
|
+
const mcpMode = shouldUseServerApi
|
|
354
|
+
? 'server-api'
|
|
355
|
+
: config.mode || (config.mcpUrl ? 'plugin' : undefined);
|
|
356
|
+
if (mcpMode === 'server-api') {
|
|
357
|
+
// Server API mode - use framer-api directly
|
|
358
|
+
await registerServerApiCommands();
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Plugin mode - use MCP transport
|
|
362
|
+
await addMcpCommands({
|
|
363
|
+
cli,
|
|
364
|
+
commandPrefix: 'mcp',
|
|
365
|
+
getMcpTransport: async (sessionId) => {
|
|
366
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
367
|
+
// UNFRAMER_MCP_URL env var overrides config file (contains full URL with auth)
|
|
368
|
+
const mcpUrl = process.env.UNFRAMER_MCP_URL || loadConfig().mcpUrl;
|
|
369
|
+
if (!mcpUrl) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const url = new URL(mcpUrl);
|
|
373
|
+
// Use /mcp endpoint for StreamableHTTP
|
|
374
|
+
if (url.pathname.endsWith('/sse')) {
|
|
375
|
+
url.pathname = url.pathname.replace(/\/sse$/, '/mcp');
|
|
376
|
+
}
|
|
377
|
+
return new StreamableHTTPClientTransport(url, { sessionId });
|
|
378
|
+
},
|
|
379
|
+
loadCache: () => {
|
|
380
|
+
return loadConfig().cachedMcpTools;
|
|
381
|
+
},
|
|
382
|
+
saveCache: (cache) => {
|
|
383
|
+
const configNow = loadConfig();
|
|
384
|
+
saveConfig({ ...configNow, cachedMcpTools: cache });
|
|
385
|
+
},
|
|
386
|
+
}).catch((e) => console.error(e));
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Register MCP commands for server-api mode using framer-api directly.
|
|
390
|
+
* This bypasses the MCP transport and calls handlers directly.
|
|
391
|
+
*/
|
|
392
|
+
async function registerServerApiCommands() {
|
|
393
|
+
// Dynamic import to avoid loading framer-api in plugin mode
|
|
394
|
+
const { connect } = await import('framer-api');
|
|
395
|
+
// `mcp eval` — run arbitrary framer-api code against a project.
|
|
396
|
+
// Useful for testing API calls before implementing them in MCP tools.
|
|
397
|
+
// The `framer` client is available as a local variable inside the evaluated code.
|
|
398
|
+
cli.command('mcp eval [code]', 'Evaluate arbitrary framer-api code against a project')
|
|
399
|
+
.option('--project <url>', 'Framer project URL. Also reads FRAMER_PROJECT_URL env var.')
|
|
400
|
+
.action(async (code, options) => {
|
|
401
|
+
// Read code from argument or stdin
|
|
402
|
+
let evalCode = code;
|
|
403
|
+
if (!evalCode && !process.stdin.isTTY) {
|
|
404
|
+
const chunks = [];
|
|
405
|
+
for await (const chunk of process.stdin) {
|
|
406
|
+
chunks.push(chunk);
|
|
407
|
+
}
|
|
408
|
+
evalCode = Buffer.concat(chunks).toString('utf-8').trim();
|
|
409
|
+
}
|
|
410
|
+
if (!evalCode) {
|
|
411
|
+
console.error('No code provided. Pass as argument or pipe via stdin.');
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
const projectUrl = options.project ||
|
|
415
|
+
process.env.FRAMER_PROJECT_URL ||
|
|
416
|
+
loadConfig().framerProjectUrl;
|
|
417
|
+
const apiKey = process.env.FRAMER_API_KEY || loadConfig().framerApiKey;
|
|
418
|
+
if (!projectUrl) {
|
|
419
|
+
console.error('Project URL required. Use --project option, FRAMER_PROJECT_URL env var, or set during login.');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
if (!apiKey) {
|
|
423
|
+
console.error('API key required. Set FRAMER_API_KEY env var or run `unframer mcp login` first.');
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
let framerClient;
|
|
427
|
+
let actionError;
|
|
428
|
+
try {
|
|
429
|
+
spinner.start('Connecting to Framer...');
|
|
430
|
+
framerClient = await connect(projectUrl, apiKey);
|
|
431
|
+
spinner.stop('Connected');
|
|
432
|
+
// Build an async function body. If the code is a single expression
|
|
433
|
+
// (no semicolons and no newlines), wrap it as a return so the result is captured.
|
|
434
|
+
const isExpression = !evalCode.includes(';') && !evalCode.includes('\n');
|
|
435
|
+
const functionBody = isExpression
|
|
436
|
+
? `return (${evalCode})`
|
|
437
|
+
: evalCode;
|
|
438
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
439
|
+
const fn = new AsyncFunction('framer', functionBody);
|
|
440
|
+
const result = await fn(framerClient);
|
|
441
|
+
if (result !== undefined) {
|
|
442
|
+
const output = (() => {
|
|
443
|
+
try {
|
|
444
|
+
return JSON.stringify(result, null, 2);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return String(result);
|
|
448
|
+
}
|
|
449
|
+
})();
|
|
450
|
+
console.log(output);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
actionError = error;
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
if (framerClient) {
|
|
458
|
+
try {
|
|
459
|
+
await framerClient.disconnect();
|
|
460
|
+
}
|
|
461
|
+
catch (disconnectError) {
|
|
462
|
+
logger.error('Failed disconnecting from Framer:', disconnectError);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (actionError) {
|
|
467
|
+
console.error('Eval failed:', actionError instanceof Error ? actionError.message : actionError);
|
|
468
|
+
if (actionError instanceof Error && actionError.stack) {
|
|
469
|
+
console.error(actionError.stack);
|
|
470
|
+
}
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
// Import tool definitions and handler from plugin-mcp
|
|
475
|
+
// Note: Run `pnpm --filter plugin-mcp build && pnpm --filter plugin-mcp gen-unframer` first
|
|
476
|
+
let mcpTools;
|
|
477
|
+
let mcpToolHandler;
|
|
478
|
+
try {
|
|
479
|
+
const imported = await import('./plugin-mcp-dist/lib/mcp-handlers.js');
|
|
480
|
+
mcpTools = imported.mcpTools;
|
|
481
|
+
mcpToolHandler = imported.mcpToolHandler;
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (hasMcpEvalCommand) {
|
|
485
|
+
// eval command only needs framer-api, not plugin-mcp handlers
|
|
486
|
+
logger.log('Could not load plugin-mcp handlers:', error instanceof Error ? error.message : error);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
throw new Error('Failed to load plugin-mcp handlers. Run: pnpm --filter plugin-mcp tsc --incremental && pnpm --filter plugin-mcp gen-unframer', { cause: error });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!mcpTools || !mcpToolHandler) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Register a command for each MCP tool
|
|
496
|
+
for (const [toolName, toolDef] of Object.entries(mcpTools)) {
|
|
497
|
+
const cmd = cli.command(`mcp ${String(toolName)}`, toolDef.description.split('\n')[0]);
|
|
498
|
+
cmd.option('--project <url>', 'Framer project URL. Uses server-api mode (framer-api headless). Works alongside plugin mode login, pass --project to switch to server-api for a single command. Also reads FRAMER_PROJECT_URL env var.');
|
|
499
|
+
// Add options based on tool input schema, using zod v4 native JSON Schema conversion
|
|
500
|
+
const inputSchema = toolDef.input;
|
|
501
|
+
if (inputSchema) {
|
|
502
|
+
const jsonSchema = z.toJSONSchema(inputSchema);
|
|
503
|
+
const properties = jsonSchema.properties || {};
|
|
504
|
+
const required = new Set(jsonSchema.required || []);
|
|
505
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
506
|
+
const isRequired = required.has(key);
|
|
507
|
+
const isBooleanType = prop.type === 'boolean';
|
|
508
|
+
const optionName = isBooleanType
|
|
509
|
+
? `--${key}`
|
|
510
|
+
: `--${key} <value>`;
|
|
511
|
+
const optionDescription = [
|
|
512
|
+
prop.description || key,
|
|
513
|
+
isRequired ? '(required)' : '',
|
|
514
|
+
]
|
|
515
|
+
.filter(Boolean)
|
|
516
|
+
.join(' ');
|
|
517
|
+
if (isBooleanType && prop.default === undefined) {
|
|
518
|
+
cmd.option(optionName, optionDescription);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
cmd.option(optionName, wrapJsonSchema({
|
|
522
|
+
...prop,
|
|
523
|
+
description: optionDescription,
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
cmd.action(async (options) => {
|
|
528
|
+
const projectOption = typeof options.project === 'string'
|
|
529
|
+
? options.project
|
|
530
|
+
: undefined;
|
|
531
|
+
const projectUrl = projectOption ||
|
|
532
|
+
process.env.FRAMER_PROJECT_URL ||
|
|
533
|
+
loadConfig().framerProjectUrl;
|
|
534
|
+
const apiKey = process.env.FRAMER_API_KEY || loadConfig().framerApiKey;
|
|
535
|
+
if (!projectUrl) {
|
|
536
|
+
console.error('Project URL required. Use --project option, FRAMER_PROJECT_URL env var, or set during login.');
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
if (!apiKey) {
|
|
540
|
+
console.error('API key required. Set FRAMER_API_KEY env var or run `unframer mcp login` first.');
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
// Remove CLI-specific options from input
|
|
544
|
+
const toolInput = Object.fromEntries(Object.entries(options).filter(([key]) => {
|
|
545
|
+
return key !== 'project';
|
|
546
|
+
}));
|
|
547
|
+
const globalWithFramer = globalThis;
|
|
548
|
+
let framerClient = undefined;
|
|
549
|
+
let actionError = undefined;
|
|
550
|
+
try {
|
|
551
|
+
spinner.start(`Connecting to Framer...`);
|
|
552
|
+
framerClient = await connect(projectUrl, apiKey);
|
|
553
|
+
// Set global framer for utility functions that use it
|
|
554
|
+
globalWithFramer.framer = framerClient;
|
|
555
|
+
spinner.start(`Running ${String(toolName)}...`);
|
|
556
|
+
const result = await mcpToolHandler({
|
|
557
|
+
type: toolName,
|
|
558
|
+
input: toolInput,
|
|
559
|
+
});
|
|
560
|
+
spinner.stop('');
|
|
561
|
+
// Output result
|
|
562
|
+
if (typeof result === 'string') {
|
|
563
|
+
console.log(result);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
console.log(JSON.stringify(result, null, 2));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
actionError = error;
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
if (framerClient) {
|
|
574
|
+
try {
|
|
575
|
+
await framerClient.disconnect();
|
|
576
|
+
}
|
|
577
|
+
catch (disconnectError) {
|
|
578
|
+
logger.error('Failed disconnecting from Framer:', disconnectError);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
delete globalWithFramer.framer;
|
|
582
|
+
}
|
|
583
|
+
if (actionError) {
|
|
584
|
+
spinner.error(`Failed: ${actionError instanceof Error ? actionError.message : String(actionError)}`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Merge multiple project configs into a single Config so esbuild bundles all
|
|
592
|
+
* components together and deduplicates shared chunks automatically.
|
|
593
|
+
*
|
|
594
|
+
* - Components maps are merged; on name collision the second project's component
|
|
595
|
+
* is prefixed with the project name to disambiguate.
|
|
596
|
+
* - First project's projectId/projectName/fullFramerProjectId are used as the
|
|
597
|
+
* primary identifiers (framerSiteId in ContextProviders).
|
|
598
|
+
* - Arrays (componentBreakpoints, tokens, locales, framerWebPages, etc.) are concatenated.
|
|
599
|
+
*/
|
|
600
|
+
function mergeConfigs(configs) {
|
|
601
|
+
if (configs.length === 0) {
|
|
602
|
+
throw new Error('mergeConfigs called with empty array');
|
|
603
|
+
}
|
|
604
|
+
if (configs.length === 1) {
|
|
605
|
+
return configs[0];
|
|
606
|
+
}
|
|
607
|
+
const first = configs[0];
|
|
608
|
+
// Merge components maps, disambiguating name collisions.
|
|
609
|
+
// Track rename maps per config so we can update breakpoints and instances.
|
|
610
|
+
const mergedComponents = {};
|
|
611
|
+
const renameMaps = [];
|
|
612
|
+
for (const config of configs) {
|
|
613
|
+
const projectSlug = kebabCase(config.projectName || config.projectId || '');
|
|
614
|
+
const renameMap = new Map();
|
|
615
|
+
renameMaps.push(renameMap);
|
|
616
|
+
for (const [name, url] of Object.entries(config.components)) {
|
|
617
|
+
if (mergedComponents[name]) {
|
|
618
|
+
// Name collision: prefix with project slug, ensure uniqueness with numeric suffix
|
|
619
|
+
const base = projectSlug ? `${projectSlug}/${name}` : `${config.projectId}/${name}`;
|
|
620
|
+
const disambiguated = uniqueComponentName(base, mergedComponents);
|
|
621
|
+
logger.log(`Component name collision for "${name}", renaming to "${disambiguated}"`);
|
|
622
|
+
renameMap.set(name, disambiguated);
|
|
623
|
+
mergedComponents[disambiguated] = url;
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
mergedComponents[name] = url;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Merge componentBreakpoints, updating componentName for any renamed components
|
|
631
|
+
const mergedBreakpoints = configs.flatMap((config, i) => {
|
|
632
|
+
const renameMap = renameMaps[i];
|
|
633
|
+
return (config.componentBreakpoints || []).map((bp) => {
|
|
634
|
+
const renamed = renameMap.get(bp.componentName);
|
|
635
|
+
if (renamed) {
|
|
636
|
+
return { ...bp, componentName: renamed };
|
|
637
|
+
}
|
|
638
|
+
return bp;
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
// Merge tokens, deduplicate by id (keep first occurrence)
|
|
642
|
+
const seenTokenIds = new Set();
|
|
643
|
+
const mergedTokens = configs.flatMap((config) => {
|
|
644
|
+
return (config.tokens || []).filter((token) => {
|
|
645
|
+
if (seenTokenIds.has(token.id)) {
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
seenTokenIds.add(token.id);
|
|
649
|
+
return true;
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
// Merge locales, deduplicate by code
|
|
653
|
+
const seenLocaleCodes = new Set();
|
|
654
|
+
const mergedLocales = configs.flatMap((config) => {
|
|
655
|
+
return (config.locales || []).filter((locale) => {
|
|
656
|
+
if (seenLocaleCodes.has(locale.code)) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
seenLocaleCodes.add(locale.code);
|
|
660
|
+
return true;
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
// Merge framerWebPages
|
|
664
|
+
const mergedWebPages = configs.flatMap((config) => {
|
|
665
|
+
return config.framerWebPages || [];
|
|
666
|
+
});
|
|
667
|
+
// Merge componentInstancesInIndexPage, updating componentPathSlug for renamed components
|
|
668
|
+
const mergedInstances = configs.flatMap((config, i) => {
|
|
669
|
+
const renameMap = renameMaps[i];
|
|
670
|
+
return (config.componentInstancesInIndexPage || []).map((instance) => {
|
|
671
|
+
const renamed = renameMap.get(instance.componentPathSlug);
|
|
672
|
+
if (renamed) {
|
|
673
|
+
return { ...instance, componentPathSlug: renamed };
|
|
674
|
+
}
|
|
675
|
+
return instance;
|
|
676
|
+
});
|
|
677
|
+
}).sort((a, b) => {
|
|
678
|
+
return a.pageOrdering - b.pageOrdering;
|
|
679
|
+
});
|
|
680
|
+
const projectNames = configs
|
|
681
|
+
.map((c) => c.projectName)
|
|
682
|
+
.filter(isTruthy);
|
|
683
|
+
spinner.info(`Merging ${configs.length} projects: ${projectNames.join(', ')}`);
|
|
684
|
+
return {
|
|
685
|
+
...first,
|
|
686
|
+
components: mergedComponents,
|
|
687
|
+
componentBreakpoints: mergedBreakpoints,
|
|
688
|
+
tokens: mergedTokens,
|
|
689
|
+
locales: mergedLocales,
|
|
690
|
+
framerWebPages: mergedWebPages,
|
|
691
|
+
componentInstancesInIndexPage: mergedInstances,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** Generate a unique component name by appending a numeric suffix if the base already exists */
|
|
695
|
+
function uniqueComponentName(base, existing) {
|
|
696
|
+
if (!existing[base]) {
|
|
697
|
+
return base;
|
|
698
|
+
}
|
|
699
|
+
let index = 2;
|
|
700
|
+
while (existing[`${base}-${index}`]) {
|
|
701
|
+
index++;
|
|
702
|
+
}
|
|
703
|
+
return `${base}-${index}`;
|
|
704
|
+
}
|
|
705
|
+
export async function configFromFetch({ projectId, externalPackages = [], allExternal = false, agent = '', outDir = undefined, }) {
|
|
706
|
+
logger.log(`Fetching config for project ${projectId}`);
|
|
707
|
+
const baseUrl = (process.env.UNFRAMER_SERVER_URL || 'https://unframer.co').replace(/\/$/, '');
|
|
708
|
+
if (process.env.UNFRAMER_SERVER_URL) {
|
|
709
|
+
console.log(`using server url ${baseUrl}`);
|
|
710
|
+
}
|
|
711
|
+
spinner.start(`Fetching config for project ${projectId}`);
|
|
712
|
+
const agentHeader = agent ||
|
|
713
|
+
(process.env.GITHUB_ACTIONS === 'true' ? 'github-actions' : 'cli');
|
|
714
|
+
const response = await fetch(`${baseUrl}/api/plugins/reactExportPlugin/project/${encodeURIComponent(projectId)}`, { headers: { 'X-Agent': agentHeader } });
|
|
715
|
+
const text = await response.text();
|
|
716
|
+
const parsed = (() => {
|
|
717
|
+
try {
|
|
718
|
+
return JSON.parse(text);
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return text;
|
|
722
|
+
}
|
|
723
|
+
})();
|
|
724
|
+
if (!response.ok) {
|
|
725
|
+
if (response.status === 402) {
|
|
726
|
+
const buyUrl = parsed?.buyUrl;
|
|
727
|
+
const message = (() => {
|
|
728
|
+
if (typeof parsed === 'object' && parsed?.message) {
|
|
729
|
+
return String(parsed.message);
|
|
730
|
+
}
|
|
731
|
+
return 'A React Export subscription is required to download components.';
|
|
732
|
+
})();
|
|
733
|
+
spinner.error(message);
|
|
734
|
+
console.info('');
|
|
735
|
+
console.info('Go to this url to buy the Framer React Export subscription:\n');
|
|
736
|
+
console.info(green(buyUrl));
|
|
737
|
+
console.info();
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
spinner.error('Error fetching project data:');
|
|
741
|
+
console.error(parsed);
|
|
742
|
+
throw new Error(`HTTP ${response.status}`, { cause: parsed });
|
|
743
|
+
}
|
|
744
|
+
const data = parsed;
|
|
745
|
+
spinner.info(`Got Framer project data`);
|
|
746
|
+
const websiteUrl = data?.project?.websiteUrl;
|
|
747
|
+
const projectName = data?.project?.projectName || '';
|
|
748
|
+
if (projectName) {
|
|
749
|
+
spinner.info(`Using project: ${projectName}`);
|
|
750
|
+
}
|
|
751
|
+
let cwd = path.resolve(process.cwd(), outDir || 'framer');
|
|
752
|
+
const indexPage = data?.framerWebPages?.find((x) => x.path === '/');
|
|
753
|
+
const componentInstancesInIndexPage = data.componentInstances
|
|
754
|
+
?.filter((x) => x.webPageId === indexPage?.webPageId)
|
|
755
|
+
.map((x) => {
|
|
756
|
+
const component = data.components.find((c) => {
|
|
757
|
+
return x.componentId === c.id;
|
|
758
|
+
});
|
|
759
|
+
if (!component) {
|
|
760
|
+
console.error(new Error(`cannot find component for instance ${x.componentId}`));
|
|
761
|
+
}
|
|
762
|
+
const componentPathSlug = componentNameToPath(component?.name || '');
|
|
763
|
+
const res = {
|
|
764
|
+
...x,
|
|
765
|
+
controls: x.controls,
|
|
766
|
+
componentPathSlug,
|
|
767
|
+
};
|
|
768
|
+
return res;
|
|
769
|
+
})
|
|
770
|
+
.sort((a, b) => {
|
|
771
|
+
return a.pageOrdering - b.pageOrdering;
|
|
772
|
+
}) || [];
|
|
773
|
+
const config = {
|
|
774
|
+
...data,
|
|
775
|
+
pageBackgroundColor: data.project?.pageBackgroundColor || '',
|
|
776
|
+
breakpoints: defaultBreakpointSizes,
|
|
777
|
+
outDir,
|
|
778
|
+
externalPackages,
|
|
779
|
+
allExternal,
|
|
780
|
+
projectId: data?.project?.projectId,
|
|
781
|
+
projectName,
|
|
782
|
+
fullFramerProjectId: data?.project?.fullFramerProjectId,
|
|
783
|
+
locales: data?.locales,
|
|
784
|
+
components: Object.fromEntries(data.components.map((c) => [componentNameToPath(c.name), c.url])),
|
|
785
|
+
componentBreakpoints: data.breakpoints
|
|
786
|
+
?.map((b) => {
|
|
787
|
+
const c = data.components.find((c) => c.id === b.componentId);
|
|
788
|
+
if (!c) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
...b,
|
|
793
|
+
componentName: componentNameToPath(c.name),
|
|
794
|
+
};
|
|
795
|
+
})
|
|
796
|
+
.filter(isTruthy) || [],
|
|
797
|
+
tokens: data.colorStyles,
|
|
798
|
+
componentInstancesInIndexPage,
|
|
799
|
+
framerWebPages: data.framerWebPages || [],
|
|
800
|
+
};
|
|
801
|
+
return { websiteUrl, cwd, config };
|
|
802
|
+
}
|
|
803
|
+
function findUp(configNames, { cwd }) {
|
|
804
|
+
let currentDir = cwd;
|
|
805
|
+
while (true) {
|
|
806
|
+
for (const configName of configNames) {
|
|
807
|
+
const configPath = path.join(currentDir, configName);
|
|
808
|
+
if (fs.existsSync(configPath)) {
|
|
809
|
+
return configPath;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const parentDir = path.dirname(currentDir);
|
|
813
|
+
if (parentDir === currentDir) {
|
|
814
|
+
// Reached the root directory
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
currentDir = parentDir;
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
//# sourceMappingURL=cli.js.map
|