unframer 4.1.8 → 4.2.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 +5 -5
- package/dist/babel-jsx.d.ts +9 -0
- package/dist/babel-jsx.d.ts.map +1 -1
- package/dist/babel-jsx.js +72 -0
- package/dist/babel-jsx.js.map +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -42
- package/dist/cli.js.map +1 -1
- package/dist/css-core.d.ts +50 -0
- package/dist/css-core.d.ts.map +1 -0
- package/dist/css-core.js +231 -0
- package/dist/css-core.js.map +1 -0
- package/dist/css.d.ts +9 -43
- package/dist/css.d.ts.map +1 -1
- package/dist/css.js +9 -232
- package/dist/css.js.map +1 -1
- package/dist/exporter.d.ts +1 -1
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +72 -11
- package/dist/exporter.js.map +1 -1
- package/dist/exporter.test.js +78 -0
- package/dist/exporter.test.js.map +1 -1
- package/dist/framer-chunks/{SqliteDatabase-VAKIICSG-R7ZS6CHH.js → SqliteDatabase-VAKIICSG-OPV4MG3Y.js} +5 -4
- package/dist/framer-chunks/{chunk-2DZGP7C2.js → chunk-7ZLQTYXQ.js} +5 -24
- package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
- package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-TVGUYU6H.js +7 -0
- package/dist/framer-chunks/fontshare-4THNDPMZ-FLLTWCDO.js +7 -0
- package/dist/framer-chunks/fontshare-B2QLD7YB-5V3XZJAH.js +7 -0
- package/dist/framer-chunks/fontshare-O22OBJ3D-FIG3CRN3.js +7 -0
- package/dist/framer-chunks/framer-font-45AI7UCZ-CKGC4MJC.js +7 -0
- package/dist/framer-chunks/google-3FCAKCAC-AM34UPJK.js +7 -0
- package/dist/framer-chunks/google-3SZHWBC6-2VTQEQ7J.js +7 -0
- package/dist/framer-chunks/google-GXDJLGJB-JNEJGCGD.js +7 -0
- package/dist/framer-chunks/sqlite-wasm-FGP37EAY-NO5QVZDI.js +7 -0
- package/dist/framer-chunks/sqlite3-SISQ6ENZ-5WRKGWTY.js +7 -0
- package/dist/framer.d.ts +128 -0
- package/dist/framer.js +820 -428
- package/dist/plugin-mcp-dist/lib/framer.d.ts +1 -7
- package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.js +90 -23
- package/dist/plugin-mcp-dist/lib/framer.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/framer.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer.test.js +214 -0
- package/dist/plugin-mcp-dist/lib/framer.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +69 -11
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/mcp.test.js +340 -364
- package/dist/plugin-mcp-dist/lib/mcp.test.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +0 -3
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/react-export.js +79 -61
- package/dist/plugin-mcp-dist/lib/react-export.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/schema.d.ts +8 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/schema.js +18 -1
- package/dist/plugin-mcp-dist/lib/schema.js.map +1 -1
- package/dist/plugin-mcp-dist/lib/store.d.ts +1 -1
- package/dist/plugin-mcp-dist/lib/store.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.js +147 -0
- package/dist/plugin-mcp-dist/lib/tunnel-integration.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.js +212 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts +1 -9
- package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
- package/dist/plugin-mcp-dist/lib/utils.js +13 -9
- package/dist/plugin-mcp-dist/lib/utils.js.map +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -13
- package/dist/react.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -8
- package/src/babel-jsx.ts +99 -0
- package/src/cli.ts +295 -42
- package/src/css-core.ts +277 -0
- package/src/css.tsx +10 -276
- package/src/exporter.test.ts +82 -0
- package/src/exporter.ts +92 -11
- package/src/framer.d.ts +128 -0
- package/src/framer.js +820 -428
- package/src/plugin-mcp-dist/lib/framer.d.ts +2 -5
- package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.js +90 -23
- package/src/plugin-mcp-dist/lib/framer.js.map +1 -1
- package/src/plugin-mcp-dist/lib/framer.test.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/framer.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/framer.test.js +213 -0
- package/src/plugin-mcp-dist/lib/framer.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +2 -2
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/mcp-handlers.js +69 -11
- package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
- package/src/plugin-mcp-dist/lib/mcp.test.js +340 -364
- package/src/plugin-mcp-dist/lib/mcp.test.js.map +1 -1
- package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/plugin-websocket.js +0 -3
- package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
- package/src/plugin-mcp-dist/lib/react-export.d.ts +1 -1
- package/src/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/react-export.js +79 -61
- package/src/plugin-mcp-dist/lib/react-export.js.map +1 -1
- package/src/plugin-mcp-dist/lib/schema.d.ts +11 -3
- package/src/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/schema.js +18 -1
- package/src/plugin-mcp-dist/lib/schema.js.map +1 -1
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.js +146 -0
- package/src/plugin-mcp-dist/lib/tunnel-integration.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/tunnel.d.ts +55 -0
- package/src/plugin-mcp-dist/lib/tunnel.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/tunnel.js +116 -0
- package/src/plugin-mcp-dist/lib/tunnel.js.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.d.ts +28 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.js +55 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.js.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.d.ts +5 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.js +211 -0
- package/src/plugin-mcp-dist/lib/upstream-socket.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/utils.d.ts +2 -2
- package/src/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
- package/src/plugin-mcp-dist/lib/utils.js +13 -9
- package/src/plugin-mcp-dist/lib/utils.js.map +1 -1
- package/src/react.tsx +7 -19
- package/src/styles/framer.css +34 -64
- package/src/version.ts +1 -1
- package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-27R5KAAW.js +0 -9
- package/dist/framer-chunks/fontshare-4THNDPMZ-BJQGNHXN.js +0 -27099
- package/dist/framer-chunks/fontshare-B2QLD7YB-4BZEAA37.js +0 -119
- package/dist/framer-chunks/fontshare-O22OBJ3D-ALBQLFE5.js +0 -825
- package/dist/framer-chunks/framer-font-45AI7UCZ-LU7DEIDM.js +0 -7
- package/dist/framer-chunks/google-3FCAKCAC-P5EL6KGL.js +0 -623876
- package/dist/framer-chunks/google-3SZHWBC6-OBXS3UIH.js +0 -1627
- package/dist/framer-chunks/google-GXDJLGJB-HHIXFE4M.js +0 -6885
- package/dist/framer-chunks/sqlite-wasm-FGP37EAY-HR6PIAJQ.js +0 -11479
- package/dist/framer-chunks/sqlite3-SISQ6ENZ-KMXYXSSV.js +0 -9
- package/dist/generated/api-client.d.ts +0 -21
- package/dist/generated/api-client.d.ts.map +0 -1
- package/dist/generated/api-client.js +0 -27
- package/dist/generated/api-client.js.map +0 -1
- package/src/generated/api-client.d.ts +0 -1238
- package/src/generated/api-client.js +0 -26
package/src/cli.ts
CHANGED
|
@@ -8,7 +8,6 @@ import './sentry.js'
|
|
|
8
8
|
import { input, select, password } from '@inquirer/prompts'
|
|
9
9
|
|
|
10
10
|
import { bundle, StyleToken, createExampleComponentCode } from './exporter.js'
|
|
11
|
-
import { createClient } from './generated/api-client.js'
|
|
12
11
|
import { generateStackblitzFiles } from './stackblitz.js'
|
|
13
12
|
|
|
14
13
|
import { goke, wrapJsonSchema } from 'goke'
|
|
@@ -17,11 +16,12 @@ import { promisify } from 'util'
|
|
|
17
16
|
|
|
18
17
|
import fs from 'fs'
|
|
19
18
|
import path, { basename } from 'path'
|
|
20
|
-
import { BreakpointSizes, defaultBreakpointSizes } from './css.js'
|
|
19
|
+
import { BreakpointSizes, defaultBreakpointSizes } from './css-core.js'
|
|
21
20
|
import {
|
|
22
21
|
componentNameToPath,
|
|
23
22
|
dedent,
|
|
24
23
|
isTruthy,
|
|
24
|
+
kebabCase,
|
|
25
25
|
logger,
|
|
26
26
|
sleep,
|
|
27
27
|
spinner,
|
|
@@ -43,7 +43,7 @@ export const cli = goke('unframer')
|
|
|
43
43
|
|
|
44
44
|
let defaultOutDir = 'framer'
|
|
45
45
|
|
|
46
|
-
cli.command('[
|
|
46
|
+
cli.command('[...projectIds]', 'Run unframer with one or more project IDs')
|
|
47
47
|
.option('--outDir <dir>', 'Output directory')
|
|
48
48
|
.option(
|
|
49
49
|
'--external [package]',
|
|
@@ -53,7 +53,7 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
|
|
|
53
53
|
.option('--jsx', 'Output jsx code instead of minified .js code')
|
|
54
54
|
.option('--debug', 'Enable debug logging')
|
|
55
55
|
.option('--metafile', 'Generate meta.json file with build metadata')
|
|
56
|
-
.action(async function main(
|
|
56
|
+
.action(async function main(projectIds, options) {
|
|
57
57
|
const outDir = options.outDir || defaultOutDir
|
|
58
58
|
const jsx = options.jsx ?? true
|
|
59
59
|
const external_ = options.external
|
|
@@ -69,17 +69,35 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
|
|
|
69
69
|
const controller = new AbortController()
|
|
70
70
|
const signal = controller.signal
|
|
71
71
|
const watch = options.watch
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
if (projectIds.length > 0) {
|
|
73
|
+
if (watch && projectIds.length > 1) {
|
|
74
|
+
console.error('--watch is only supported with a single project ID')
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fetch configs for all project IDs
|
|
79
|
+
const fetchResults = await Promise.all(
|
|
80
|
+
projectIds.map((projectId) => {
|
|
81
|
+
return configFromFetch({
|
|
82
|
+
allExternal,
|
|
83
|
+
externalPackages,
|
|
84
|
+
outDir,
|
|
85
|
+
projectId,
|
|
86
|
+
})
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Merge configs when multiple projects are provided, otherwise use single config directly
|
|
91
|
+
const mergedConfig = fetchResults.length === 1
|
|
92
|
+
? fetchResults[0].config
|
|
93
|
+
: mergeConfigs(fetchResults.map((r) => r.config))
|
|
94
|
+
const cwd = fetchResults[0].cwd
|
|
95
|
+
const websiteUrl = fetchResults[0].websiteUrl
|
|
96
|
+
|
|
79
97
|
const { rebuild, buildContext } = await bundle({
|
|
80
98
|
config: {
|
|
81
99
|
jsx,
|
|
82
|
-
...
|
|
100
|
+
...mergedConfig,
|
|
83
101
|
},
|
|
84
102
|
watch,
|
|
85
103
|
cwd,
|
|
@@ -420,14 +438,15 @@ const hasMcpCommand = cliArgs.includes('mcp')
|
|
|
420
438
|
const hasProjectOption = cliArgs.some((arg) => {
|
|
421
439
|
return arg === '--project' || arg.startsWith('--project=')
|
|
422
440
|
})
|
|
423
|
-
const
|
|
424
|
-
const
|
|
441
|
+
const hasMcpEvalCommand = hasMcpCommand && cliArgs.includes('eval')
|
|
442
|
+
const hasServerApiEnv = Boolean(process.env.FRAMER_PROJECT_URL || process.env.FRAMER_API_KEY)
|
|
443
|
+
const shouldUseServerApi = hasMcpCommand && (hasProjectOption || hasMcpEvalCommand || hasServerApiEnv)
|
|
444
|
+
const mcpMode = shouldUseServerApi
|
|
425
445
|
? 'server-api'
|
|
426
446
|
: config.mode || (config.mcpUrl ? 'plugin' : undefined)
|
|
427
447
|
|
|
428
448
|
if (mcpMode === 'server-api') {
|
|
429
449
|
// Server API mode - use framer-api directly
|
|
430
|
-
// Commands are registered via registerServerApiCommands below
|
|
431
450
|
await registerServerApiCommands()
|
|
432
451
|
} else {
|
|
433
452
|
// Plugin mode - use MCP transport
|
|
@@ -468,9 +487,112 @@ async function registerServerApiCommands() {
|
|
|
468
487
|
// Dynamic import to avoid loading framer-api in plugin mode
|
|
469
488
|
const { connect } = await import('framer-api')
|
|
470
489
|
|
|
490
|
+
// `mcp eval` — run arbitrary framer-api code against a project.
|
|
491
|
+
// Useful for testing API calls before implementing them in MCP tools.
|
|
492
|
+
// The `framer` client is available as a local variable inside the evaluated code.
|
|
493
|
+
cli.command('mcp eval [code]', 'Evaluate arbitrary framer-api code against a project')
|
|
494
|
+
.option(
|
|
495
|
+
'--project <url>',
|
|
496
|
+
'Framer project URL. Also reads FRAMER_PROJECT_URL env var.',
|
|
497
|
+
)
|
|
498
|
+
.action(async (code: string | undefined, options: { project?: string }) => {
|
|
499
|
+
// Read code from argument or stdin
|
|
500
|
+
let evalCode = code
|
|
501
|
+
if (!evalCode && !process.stdin.isTTY) {
|
|
502
|
+
const chunks: Buffer[] = []
|
|
503
|
+
for await (const chunk of process.stdin) {
|
|
504
|
+
chunks.push(chunk)
|
|
505
|
+
}
|
|
506
|
+
evalCode = Buffer.concat(chunks).toString('utf-8').trim()
|
|
507
|
+
}
|
|
508
|
+
if (!evalCode) {
|
|
509
|
+
console.error('No code provided. Pass as argument or pipe via stdin.')
|
|
510
|
+
process.exit(1)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const projectUrl =
|
|
514
|
+
options.project ||
|
|
515
|
+
process.env.FRAMER_PROJECT_URL ||
|
|
516
|
+
loadConfig().framerProjectUrl
|
|
517
|
+
const apiKey =
|
|
518
|
+
process.env.FRAMER_API_KEY || loadConfig().framerApiKey
|
|
519
|
+
|
|
520
|
+
if (!projectUrl) {
|
|
521
|
+
console.error('Project URL required. Use --project option, FRAMER_PROJECT_URL env var, or set during login.')
|
|
522
|
+
process.exit(1)
|
|
523
|
+
}
|
|
524
|
+
if (!apiKey) {
|
|
525
|
+
console.error('API key required. Set FRAMER_API_KEY env var or run `unframer mcp login` first.')
|
|
526
|
+
process.exit(1)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let framerClient: Awaited<ReturnType<typeof connect>> | undefined
|
|
530
|
+
let actionError: unknown
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
spinner.start('Connecting to Framer...')
|
|
534
|
+
framerClient = await connect(projectUrl, apiKey)
|
|
535
|
+
spinner.stop('Connected')
|
|
536
|
+
|
|
537
|
+
// Build an async function body. If the code is a single expression
|
|
538
|
+
// (no semicolons and no newlines), wrap it as a return so the result is captured.
|
|
539
|
+
const isExpression = !evalCode.includes(';') && !evalCode.includes('\n')
|
|
540
|
+
const functionBody = isExpression
|
|
541
|
+
? `return (${evalCode})`
|
|
542
|
+
: evalCode
|
|
543
|
+
|
|
544
|
+
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor as
|
|
545
|
+
new (arg: string, body: string) => (framer: unknown) => Promise<unknown>
|
|
546
|
+
const fn = new AsyncFunction('framer', functionBody)
|
|
547
|
+
const result = await fn(framerClient)
|
|
548
|
+
|
|
549
|
+
if (result !== undefined) {
|
|
550
|
+
const output = (() => {
|
|
551
|
+
try {
|
|
552
|
+
return JSON.stringify(result, null, 2)
|
|
553
|
+
} catch {
|
|
554
|
+
return String(result)
|
|
555
|
+
}
|
|
556
|
+
})()
|
|
557
|
+
console.log(output)
|
|
558
|
+
}
|
|
559
|
+
} catch (error) {
|
|
560
|
+
actionError = error
|
|
561
|
+
} finally {
|
|
562
|
+
if (framerClient) {
|
|
563
|
+
try {
|
|
564
|
+
await framerClient.disconnect()
|
|
565
|
+
} catch (disconnectError) {
|
|
566
|
+
logger.error('Failed disconnecting from Framer:', disconnectError)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (actionError) {
|
|
572
|
+
console.error('Eval failed:', actionError instanceof Error ? actionError.message : actionError)
|
|
573
|
+
if (actionError instanceof Error && actionError.stack) {
|
|
574
|
+
console.error(actionError.stack)
|
|
575
|
+
}
|
|
576
|
+
process.exit(1)
|
|
577
|
+
}
|
|
578
|
+
})
|
|
579
|
+
|
|
471
580
|
// Import tool definitions and handler from plugin-mcp
|
|
472
581
|
// Note: Run `pnpm --filter plugin-mcp build && pnpm --filter plugin-mcp gen-unframer` first
|
|
473
|
-
|
|
582
|
+
let mcpTools: Awaited<typeof import('./plugin-mcp-dist/lib/mcp-handlers.js')>['mcpTools'] | undefined
|
|
583
|
+
let mcpToolHandler: Awaited<typeof import('./plugin-mcp-dist/lib/mcp-handlers.js')>['mcpToolHandler'] | undefined
|
|
584
|
+
try {
|
|
585
|
+
const imported = await import('./plugin-mcp-dist/lib/mcp-handlers.js')
|
|
586
|
+
mcpTools = imported.mcpTools
|
|
587
|
+
mcpToolHandler = imported.mcpToolHandler
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (hasMcpEvalCommand) {
|
|
590
|
+
// eval command only needs framer-api, not plugin-mcp handlers
|
|
591
|
+
logger.log('Could not load plugin-mcp handlers:', error instanceof Error ? error.message : error)
|
|
592
|
+
} else {
|
|
593
|
+
throw new Error('Failed to load plugin-mcp handlers. Run: pnpm --filter plugin-mcp tsc --incremental && pnpm --filter plugin-mcp gen-unframer', { cause: error })
|
|
594
|
+
}
|
|
595
|
+
}
|
|
474
596
|
if (!mcpTools || !mcpToolHandler) {
|
|
475
597
|
return
|
|
476
598
|
}
|
|
@@ -614,6 +736,7 @@ async function registerServerApiCommands() {
|
|
|
614
736
|
}
|
|
615
737
|
})
|
|
616
738
|
}
|
|
739
|
+
|
|
617
740
|
}
|
|
618
741
|
|
|
619
742
|
export type Config = {
|
|
@@ -663,6 +786,133 @@ type ComponentInstanceInPage = {
|
|
|
663
786
|
webPageId: string
|
|
664
787
|
}
|
|
665
788
|
|
|
789
|
+
/**
|
|
790
|
+
* Merge multiple project configs into a single Config so esbuild bundles all
|
|
791
|
+
* components together and deduplicates shared chunks automatically.
|
|
792
|
+
*
|
|
793
|
+
* - Components maps are merged; on name collision the second project's component
|
|
794
|
+
* is prefixed with the project name to disambiguate.
|
|
795
|
+
* - First project's projectId/projectName/fullFramerProjectId are used as the
|
|
796
|
+
* primary identifiers (framerSiteId in ContextProviders).
|
|
797
|
+
* - Arrays (componentBreakpoints, tokens, locales, framerWebPages, etc.) are concatenated.
|
|
798
|
+
*/
|
|
799
|
+
function mergeConfigs(configs: Config[]): Config {
|
|
800
|
+
if (configs.length === 0) {
|
|
801
|
+
throw new Error('mergeConfigs called with empty array')
|
|
802
|
+
}
|
|
803
|
+
if (configs.length === 1) {
|
|
804
|
+
return configs[0]
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const first = configs[0]
|
|
808
|
+
|
|
809
|
+
// Merge components maps, disambiguating name collisions.
|
|
810
|
+
// Track rename maps per config so we can update breakpoints and instances.
|
|
811
|
+
const mergedComponents: Record<string, string> = {}
|
|
812
|
+
const renameMaps: Map<string, string>[] = []
|
|
813
|
+
|
|
814
|
+
for (const config of configs) {
|
|
815
|
+
const projectSlug = kebabCase(config.projectName || config.projectId || '')
|
|
816
|
+
const renameMap = new Map<string, string>()
|
|
817
|
+
renameMaps.push(renameMap)
|
|
818
|
+
|
|
819
|
+
for (const [name, url] of Object.entries(config.components)) {
|
|
820
|
+
if (mergedComponents[name]) {
|
|
821
|
+
// Name collision: prefix with project slug, ensure uniqueness with numeric suffix
|
|
822
|
+
const base = projectSlug ? `${projectSlug}/${name}` : `${config.projectId}/${name}`
|
|
823
|
+
const disambiguated = uniqueComponentName(base, mergedComponents)
|
|
824
|
+
logger.log(`Component name collision for "${name}", renaming to "${disambiguated}"`)
|
|
825
|
+
renameMap.set(name, disambiguated)
|
|
826
|
+
mergedComponents[disambiguated] = url
|
|
827
|
+
} else {
|
|
828
|
+
mergedComponents[name] = url
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Merge componentBreakpoints, updating componentName for any renamed components
|
|
834
|
+
const mergedBreakpoints = configs.flatMap((config, i) => {
|
|
835
|
+
const renameMap = renameMaps[i]
|
|
836
|
+
return (config.componentBreakpoints || []).map((bp) => {
|
|
837
|
+
const renamed = renameMap.get(bp.componentName)
|
|
838
|
+
if (renamed) {
|
|
839
|
+
return { ...bp, componentName: renamed }
|
|
840
|
+
}
|
|
841
|
+
return bp
|
|
842
|
+
})
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
// Merge tokens, deduplicate by id (keep first occurrence)
|
|
846
|
+
const seenTokenIds = new Set<string>()
|
|
847
|
+
const mergedTokens = configs.flatMap((config) => {
|
|
848
|
+
return (config.tokens || []).filter((token) => {
|
|
849
|
+
if (seenTokenIds.has(token.id)) {
|
|
850
|
+
return false
|
|
851
|
+
}
|
|
852
|
+
seenTokenIds.add(token.id)
|
|
853
|
+
return true
|
|
854
|
+
})
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
// Merge locales, deduplicate by code
|
|
858
|
+
const seenLocaleCodes = new Set<string>()
|
|
859
|
+
const mergedLocales = configs.flatMap((config) => {
|
|
860
|
+
return (config.locales || []).filter((locale) => {
|
|
861
|
+
if (seenLocaleCodes.has(locale.code)) {
|
|
862
|
+
return false
|
|
863
|
+
}
|
|
864
|
+
seenLocaleCodes.add(locale.code)
|
|
865
|
+
return true
|
|
866
|
+
})
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
// Merge framerWebPages
|
|
870
|
+
const mergedWebPages = configs.flatMap((config) => {
|
|
871
|
+
return config.framerWebPages || []
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
// Merge componentInstancesInIndexPage, updating componentPathSlug for renamed components
|
|
875
|
+
const mergedInstances = configs.flatMap((config, i) => {
|
|
876
|
+
const renameMap = renameMaps[i]
|
|
877
|
+
return (config.componentInstancesInIndexPage || []).map((instance) => {
|
|
878
|
+
const renamed = renameMap.get(instance.componentPathSlug)
|
|
879
|
+
if (renamed) {
|
|
880
|
+
return { ...instance, componentPathSlug: renamed }
|
|
881
|
+
}
|
|
882
|
+
return instance
|
|
883
|
+
})
|
|
884
|
+
}).sort((a, b) => {
|
|
885
|
+
return a.pageOrdering - b.pageOrdering
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
const projectNames = configs
|
|
889
|
+
.map((c) => c.projectName)
|
|
890
|
+
.filter(isTruthy)
|
|
891
|
+
spinner.info(`Merging ${configs.length} projects: ${projectNames.join(', ')}`)
|
|
892
|
+
|
|
893
|
+
return {
|
|
894
|
+
...first,
|
|
895
|
+
components: mergedComponents,
|
|
896
|
+
componentBreakpoints: mergedBreakpoints,
|
|
897
|
+
tokens: mergedTokens,
|
|
898
|
+
locales: mergedLocales,
|
|
899
|
+
framerWebPages: mergedWebPages,
|
|
900
|
+
componentInstancesInIndexPage: mergedInstances,
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Generate a unique component name by appending a numeric suffix if the base already exists */
|
|
905
|
+
function uniqueComponentName(base: string, existing: Record<string, string>): string {
|
|
906
|
+
if (!existing[base]) {
|
|
907
|
+
return base
|
|
908
|
+
}
|
|
909
|
+
let index = 2
|
|
910
|
+
while (existing[`${base}-${index}`]) {
|
|
911
|
+
index++
|
|
912
|
+
}
|
|
913
|
+
return `${base}-${index}`
|
|
914
|
+
}
|
|
915
|
+
|
|
666
916
|
export async function configFromFetch({
|
|
667
917
|
projectId,
|
|
668
918
|
externalPackages = [] as string[],
|
|
@@ -672,38 +922,41 @@ export async function configFromFetch({
|
|
|
672
922
|
}) {
|
|
673
923
|
logger.log(`Fetching config for project ${projectId}`)
|
|
674
924
|
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
925
|
+
const baseUrl = (
|
|
926
|
+
process.env.UNFRAMER_SERVER_URL || 'https://unframer.co'
|
|
927
|
+
).replace(/\/$/, '')
|
|
928
|
+
if (process.env.UNFRAMER_SERVER_URL) {
|
|
929
|
+
console.log(`using server url ${baseUrl}`)
|
|
678
930
|
}
|
|
679
|
-
const client = await createClient({
|
|
680
|
-
url: url || 'https://unframer.co',
|
|
681
|
-
headers: {
|
|
682
|
-
'X-Agent':
|
|
683
|
-
agent ||
|
|
684
|
-
(process.env.GITHUB_ACTIONS === 'true'
|
|
685
|
-
? 'github-actions'
|
|
686
|
-
: 'cli'),
|
|
687
|
-
},
|
|
688
|
-
})
|
|
689
931
|
|
|
690
932
|
spinner.start(`Fetching config for project ${projectId}`)
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
933
|
+
const agentHeader =
|
|
934
|
+
agent ||
|
|
935
|
+
(process.env.GITHUB_ACTIONS === 'true' ? 'github-actions' : 'cli')
|
|
936
|
+
const response = await fetch(
|
|
937
|
+
`${baseUrl}/api/plugins/reactExportPlugin/project/${encodeURIComponent(projectId)}`,
|
|
938
|
+
{ headers: { 'X-Agent': agentHeader } },
|
|
939
|
+
)
|
|
940
|
+
const text = await response.text()
|
|
941
|
+
const parsed = (() => {
|
|
942
|
+
try {
|
|
943
|
+
return JSON.parse(text)
|
|
944
|
+
} catch {
|
|
945
|
+
return text
|
|
946
|
+
}
|
|
947
|
+
})()
|
|
948
|
+
|
|
949
|
+
if (!response.ok) {
|
|
950
|
+
if (response.status === 402) {
|
|
951
|
+
const buyUrl = parsed?.buyUrl
|
|
698
952
|
|
|
699
953
|
const message = (() => {
|
|
700
|
-
if (typeof
|
|
701
|
-
return String(
|
|
954
|
+
if (typeof parsed === 'object' && parsed?.message) {
|
|
955
|
+
return String(parsed.message)
|
|
702
956
|
}
|
|
703
957
|
return 'A React Export subscription is required to download components.'
|
|
704
958
|
})()
|
|
705
959
|
|
|
706
|
-
// spinner.error(details)
|
|
707
960
|
spinner.error(message)
|
|
708
961
|
console.info('')
|
|
709
962
|
console.info(
|
|
@@ -712,12 +965,12 @@ export async function configFromFetch({
|
|
|
712
965
|
console.info(green(buyUrl))
|
|
713
966
|
console.info()
|
|
714
967
|
process.exit(1)
|
|
715
|
-
// throw new Error(details, { cause: error })
|
|
716
968
|
}
|
|
717
969
|
spinner.error('Error fetching project data:')
|
|
718
|
-
console.error(
|
|
719
|
-
throw
|
|
970
|
+
console.error(parsed)
|
|
971
|
+
throw new Error(`HTTP ${response.status}`, { cause: parsed })
|
|
720
972
|
}
|
|
973
|
+
const data = parsed
|
|
721
974
|
spinner.info(`Got Framer project data`)
|
|
722
975
|
const websiteUrl = data?.project?.websiteUrl
|
|
723
976
|
|