unframer 4.1.9 → 4.2.1

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.
Files changed (100) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +238 -15
  4. package/dist/cli.js.map +1 -1
  5. package/dist/css.d.ts +1 -1
  6. package/dist/css.d.ts.map +1 -1
  7. package/dist/exporter.d.ts.map +1 -1
  8. package/dist/exporter.js +29 -5
  9. package/dist/exporter.js.map +1 -1
  10. package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
  11. package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
  12. package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
  13. package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
  14. package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
  15. package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
  16. package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
  17. package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
  18. package/dist/framer.d.ts +128 -0
  19. package/dist/framer.js +15926 -11214
  20. package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -1
  21. package/dist/plugin-mcp-dist/lib/framer-client.server.js +9 -0
  22. package/dist/plugin-mcp-dist/lib/framer-client.server.js.map +1 -1
  23. package/dist/plugin-mcp-dist/lib/framer.d.ts +8 -1
  24. package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
  25. package/dist/plugin-mcp-dist/lib/framer.js +117 -21
  26. package/dist/plugin-mcp-dist/lib/framer.js.map +1 -1
  27. package/dist/plugin-mcp-dist/lib/framer.test.js +79 -65
  28. package/dist/plugin-mcp-dist/lib/framer.test.js.map +1 -1
  29. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +12 -12
  30. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
  31. package/dist/plugin-mcp-dist/lib/mcp-handlers.js +247 -54
  32. package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
  33. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
  34. package/dist/plugin-mcp-dist/lib/plugin-websocket.js +36 -25
  35. package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
  36. package/dist/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
  37. package/dist/plugin-mcp-dist/lib/react-export.js +79 -61
  38. package/dist/plugin-mcp-dist/lib/react-export.js.map +1 -1
  39. package/dist/plugin-mcp-dist/lib/schema.d.ts +8 -0
  40. package/dist/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
  41. package/dist/plugin-mcp-dist/lib/schema.js +18 -1
  42. package/dist/plugin-mcp-dist/lib/schema.js.map +1 -1
  43. package/dist/plugin-mcp-dist/lib/store.d.ts +1 -1
  44. package/dist/plugin-mcp-dist/lib/store.d.ts.map +1 -1
  45. package/dist/plugin-mcp-dist/lib/utils.d.ts +1 -1
  46. package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
  47. package/dist/plugin-mcp-dist/lib/utils.js +11 -7
  48. package/dist/plugin-mcp-dist/lib/utils.js.map +1 -1
  49. package/dist/react.d.ts +3 -3
  50. package/dist/react.d.ts.map +1 -1
  51. package/dist/react.js +0 -2
  52. package/dist/react.js.map +1 -1
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +6 -6
  56. package/src/cli.ts +265 -14
  57. package/src/exporter.ts +39 -6
  58. package/src/framer.d.ts +128 -0
  59. package/src/framer.js +15926 -11214
  60. package/src/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -1
  61. package/src/plugin-mcp-dist/lib/framer-client.server.js +9 -0
  62. package/src/plugin-mcp-dist/lib/framer-client.server.js.map +1 -1
  63. package/src/plugin-mcp-dist/lib/framer.d.ts +10 -1
  64. package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -1
  65. package/src/plugin-mcp-dist/lib/framer.js +117 -21
  66. package/src/plugin-mcp-dist/lib/framer.js.map +1 -1
  67. package/src/plugin-mcp-dist/lib/framer.test.js +79 -65
  68. package/src/plugin-mcp-dist/lib/framer.test.js.map +1 -1
  69. package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +4 -4
  70. package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -1
  71. package/src/plugin-mcp-dist/lib/mcp-handlers.js +247 -54
  72. package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -1
  73. package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -1
  74. package/src/plugin-mcp-dist/lib/plugin-websocket.js +36 -25
  75. package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -1
  76. package/src/plugin-mcp-dist/lib/react-export.d.ts.map +1 -1
  77. package/src/plugin-mcp-dist/lib/react-export.js +79 -61
  78. package/src/plugin-mcp-dist/lib/react-export.js.map +1 -1
  79. package/src/plugin-mcp-dist/lib/schema.d.ts +8 -0
  80. package/src/plugin-mcp-dist/lib/schema.d.ts.map +1 -1
  81. package/src/plugin-mcp-dist/lib/schema.js +18 -1
  82. package/src/plugin-mcp-dist/lib/schema.js.map +1 -1
  83. package/src/plugin-mcp-dist/lib/utils.d.ts.map +1 -1
  84. package/src/plugin-mcp-dist/lib/utils.js +11 -7
  85. package/src/plugin-mcp-dist/lib/utils.js.map +1 -1
  86. package/src/react.tsx +0 -3
  87. package/src/styles/framer.css +36 -120
  88. package/src/version.ts +1 -1
  89. package/dist/framer-chunks/SqliteDatabase-VAKIICSG-W43ZSXBO.js +0 -97
  90. package/dist/framer-chunks/chunk-WYG6DFEF.js +0 -121
  91. package/dist/framer-chunks/default-blog-sqlite-7ZHEY3GT-DXFOTMBP.js +0 -9
  92. package/dist/framer-chunks/fontshare-4THNDPMZ-L3NZDIOE.js +0 -27099
  93. package/dist/framer-chunks/fontshare-B2QLD7YB-ZLNQ44LW.js +0 -119
  94. package/dist/framer-chunks/fontshare-O22OBJ3D-VY7WF3BB.js +0 -825
  95. package/dist/framer-chunks/framer-font-45AI7UCZ-Z3XHDH5K.js +0 -7
  96. package/dist/framer-chunks/google-3FCAKCAC-K2ZVMKHN.js +0 -623876
  97. package/dist/framer-chunks/google-3SZHWBC6-MIC5SCB4.js +0 -1627
  98. package/dist/framer-chunks/google-GXDJLGJB-356NWSZ7.js +0 -6885
  99. package/dist/framer-chunks/sqlite-wasm-FGP37EAY-MBPG3MPB.js +0 -11479
  100. package/dist/framer-chunks/sqlite3-SISQ6ENZ-RRHGROT5.js +0 -9
package/src/cli.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  componentNameToPath,
22
22
  dedent,
23
23
  isTruthy,
24
+ kebabCase,
24
25
  logger,
25
26
  sleep,
26
27
  spinner,
@@ -42,7 +43,7 @@ export const cli = goke('unframer')
42
43
 
43
44
  let defaultOutDir = 'framer'
44
45
 
45
- cli.command('[projectId]', 'Run unframer with optional project ID')
46
+ cli.command('[...projectIds]', 'Run unframer with one or more project IDs')
46
47
  .option('--outDir <dir>', 'Output directory')
47
48
  .option(
48
49
  '--external [package]',
@@ -52,7 +53,7 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
52
53
  .option('--jsx', 'Output jsx code instead of minified .js code')
53
54
  .option('--debug', 'Enable debug logging')
54
55
  .option('--metafile', 'Generate meta.json file with build metadata')
55
- .action(async function main(projectId, options) {
56
+ .action(async function main(projectIds, options) {
56
57
  const outDir = options.outDir || defaultOutDir
57
58
  const jsx = options.jsx ?? true
58
59
  const external_ = options.external
@@ -68,17 +69,35 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
68
69
  const controller = new AbortController()
69
70
  const signal = controller.signal
70
71
  const watch = options.watch
71
- if (projectId) {
72
- const { config, cwd, websiteUrl } = await configFromFetch({
73
- allExternal,
74
- externalPackages,
75
- outDir,
76
- projectId,
77
- })
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
+
78
97
  const { rebuild, buildContext } = await bundle({
79
98
  config: {
80
99
  jsx,
81
- ...config,
100
+ ...mergedConfig,
82
101
  },
83
102
  watch,
84
103
  cwd,
@@ -419,14 +438,15 @@ const hasMcpCommand = cliArgs.includes('mcp')
419
438
  const hasProjectOption = cliArgs.some((arg) => {
420
439
  return arg === '--project' || arg.startsWith('--project=')
421
440
  })
422
- const shouldUseServerApiForProjectOption = hasMcpCommand && hasProjectOption
423
- const mcpMode = shouldUseServerApiForProjectOption
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
424
445
  ? 'server-api'
425
446
  : config.mode || (config.mcpUrl ? 'plugin' : undefined)
426
447
 
427
448
  if (mcpMode === 'server-api') {
428
449
  // Server API mode - use framer-api directly
429
- // Commands are registered via registerServerApiCommands below
430
450
  await registerServerApiCommands()
431
451
  } else {
432
452
  // Plugin mode - use MCP transport
@@ -467,9 +487,112 @@ async function registerServerApiCommands() {
467
487
  // Dynamic import to avoid loading framer-api in plugin mode
468
488
  const { connect } = await import('framer-api')
469
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
+
470
580
  // Import tool definitions and handler from plugin-mcp
471
581
  // Note: Run `pnpm --filter plugin-mcp build && pnpm --filter plugin-mcp gen-unframer` first
472
- const { mcpTools, mcpToolHandler } = await import('./plugin-mcp-dist/lib/mcp-handlers.js')
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
+ }
473
596
  if (!mcpTools || !mcpToolHandler) {
474
597
  return
475
598
  }
@@ -613,6 +736,7 @@ async function registerServerApiCommands() {
613
736
  }
614
737
  })
615
738
  }
739
+
616
740
  }
617
741
 
618
742
  export type Config = {
@@ -662,6 +786,133 @@ type ComponentInstanceInPage = {
662
786
  webPageId: string
663
787
  }
664
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
+
665
916
  export async function configFromFetch({
666
917
  projectId,
667
918
  externalPackages = [] as string[],
package/src/exporter.ts CHANGED
@@ -8,8 +8,6 @@ const require = createRequire(import.meta.url)
8
8
 
9
9
  import { Sema } from 'async-sema'
10
10
 
11
- import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'
12
-
13
11
  import { transform } from '@babel/core'
14
12
  import { exec } from 'child_process'
15
13
  import { error } from 'console'
@@ -219,7 +217,6 @@ export async function bundle({
219
217
  missingPackages.add(pkg)
220
218
  },
221
219
  }),
222
- nodeModulesPolyfillPlugin({}),
223
220
  {
224
221
  name: 'virtual loader',
225
222
  setup(build) {
@@ -577,17 +574,28 @@ export async function bundle({
577
574
  ?.split('\n')
578
575
  .forEach((x) => logger.log(x))
579
576
 
577
+ function isChunkPath(filePath: string) {
578
+ return (
579
+ filePath.includes('/chunks/') ||
580
+ filePath.includes('\\chunks\\')
581
+ )
582
+ }
580
583
  const jsxFiles = buildResult.outputFiles
581
584
  .filter(
582
585
  (x) =>
583
586
  x.path.endsWith('.js') &&
587
+ !isChunkPath(x.path) &&
584
588
  fs.existsSync(getFilePaths(x.path, out).jsxPath),
585
589
  )
586
590
  .map((x) => getFilePaths(x.path, out).jsxPath)
587
591
  const outFiles = buildResult.outputFiles
588
592
  .map((x) => {
589
593
  const paths = getFilePaths(x.path, out)
590
- if (x.path.endsWith('.js') && fs.existsSync(paths.jsxPath)) {
594
+ if (
595
+ x.path.endsWith('.js') &&
596
+ !isChunkPath(x.path) &&
597
+ fs.existsSync(paths.jsxPath)
598
+ ) {
591
599
  return null // Will be handled by jsx files
592
600
  }
593
601
  return paths.finalJsPath
@@ -737,8 +745,17 @@ export async function bundle({
737
745
  formatted = result.content
738
746
  }
739
747
  } catch (e) {
748
+ spinner.warn(
749
+ `Babel/Biome formatting failed for ${componentName}: ${e instanceof Error ? e.message : e}`,
750
+ )
751
+ logger.log(e instanceof Error ? e.stack : e)
740
752
  notifyError(e, 'babel transform and format')
741
753
  }
754
+ } else if (tooBig) {
755
+ const sizeMB = (file.text.length / (1024 * 1024)).toFixed(2)
756
+ spinner.warn(
757
+ `Skipping JSX formatting for ${componentName} (${sizeMB} MB exceeds ${(tooBigSize / (1024 * 1024)).toFixed(2)} MB limit)`,
758
+ )
742
759
  }
743
760
 
744
761
  const prefix =
@@ -752,12 +769,24 @@ export async function bundle({
752
769
  // Always write the temp .js file for type extraction
753
770
  await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8')
754
771
 
755
- // Only write .jsx file if it's different from existing or if formatting was done
772
+ // Write .jsx file if formatting succeeded, or fall back to writing
773
+ // unformatted .js content into the .jsx path when formatting fails
774
+ // (babel error, component too big, etc.) but a stale .jsx already
775
+ // exists on disk. Without this fallback the stale .jsx keeps old
776
+ // chunk imports while the old chunks are deleted, breaking the build.
756
777
  if (didFormat && codeJsx !== existing) {
757
778
  await fs.promises.writeFile(paths.jsxPath, codeJsx, 'utf-8')
758
779
  logger.log(`Updated JSX file for ${componentName}`)
759
780
  } else if (didFormat) {
760
781
  logger.log(`JSX file unchanged for ${componentName}`)
782
+ } else if (existing && codeJs !== existing) {
783
+ // Formatting failed but .jsx exists with stale content (e.g.
784
+ // old chunk hashes). Overwrite with unformatted JS so imports
785
+ // stay in sync with the current build's chunks.
786
+ await fs.promises.writeFile(paths.jsxPath, codeJs, 'utf-8')
787
+ logger.log(
788
+ `Updated JSX file (unformatted) for ${componentName}`,
789
+ )
761
790
  }
762
791
  }
763
792
  spinner.stop()
@@ -772,8 +801,12 @@ export async function bundle({
772
801
  if (file.path.endsWith('.js')) {
773
802
  const paths = getFilePaths(file.path, out)
774
803
 
775
- if (fs.existsSync(paths.jsxPath)) {
804
+ if (
805
+ !isChunkPath(paths.tempJsPath) &&
806
+ fs.existsSync(paths.jsxPath)
807
+ ) {
776
808
  // Remove temp .js file if .jsx equivalent exists
809
+ // (never applies to chunks which stay as .js)
777
810
  logger.log(
778
811
  'removing temp JS file with JSX equivalent:',
779
812
  path.relative(out, paths.tempJsPath),
package/src/framer.d.ts CHANGED
@@ -4859,4 +4859,132 @@ export declare const withVariantFX: <T extends MotionProps>(Component: React_2.C
4859
4859
  export { }
4860
4860
  export declare const combinedCSSRules: string[]
4861
4861
 
4862
+ export interface FramerMotionGlobalConfig {
4863
+ useManualTiming?: boolean
4864
+ instantAnimations?: boolean
4865
+ skipAnimations?: boolean
4866
+ mix?: unknown
4867
+ WillChange?: new (value: string) => unknown
4868
+ }
4869
+
4870
+ export interface FramerInjectedRuntimeImage {
4871
+ src?: string
4872
+ srcSet?: string
4873
+ }
4874
+
4875
+ export interface FramerInjectedRuntimeRect {
4876
+ width?: number
4877
+ height?: number
4878
+ }
4879
+
4880
+ export interface FramerInjectedRuntime {
4881
+ imagePlaceholderSvg?: string
4882
+ useImageSource?(image: FramerInjectedRuntimeImage, rect?: FramerInjectedRuntimeRect, nodeId?: string): string
4883
+ useImageElement?(image: FramerInjectedRuntimeImage, rect?: FramerInjectedRuntimeRect, nodeId?: string): HTMLImageElement
4884
+ canRenderOptimizedCanvasImage?(src?: string): boolean
4885
+ isOnPageCanvas?: boolean
4886
+ }
4887
+
4888
+ export interface FramerMotionFrameData {
4889
+ delta: number
4890
+ timestamp: number
4891
+ isProcessing: boolean
4892
+ }
4893
+
4894
+ export type FramerMotionFrameCallback = (frameData: FramerMotionFrameData) => void
4895
+
4896
+ export interface FramerMotionFrameStep {
4897
+ schedule(callback: FramerMotionFrameCallback, keepAlive?: boolean, immediate?: boolean): FramerMotionFrameCallback
4898
+ cancel(callback: FramerMotionFrameCallback): void
4899
+ process(frameData: FramerMotionFrameData): void
4900
+ }
4901
+
4902
+ export type FramerMotionFrameStepName =
4903
+ | 'setup'
4904
+ | 'read'
4905
+ | 'resolveKeyframes'
4906
+ | 'preUpdate'
4907
+ | 'update'
4908
+ | 'preRender'
4909
+ | 'render'
4910
+ | 'postRender'
4911
+
4912
+ export type FramerMotionFrameSteps = Record<FramerMotionFrameStepName, FramerMotionFrameStep>
4913
+
4914
+ export interface FramerMotionTime {
4915
+ now(): number
4916
+ set(newTime: number): void
4917
+ }
4918
+
4919
+ export interface FramerMotionGeneratorState<Value = unknown> {
4920
+ value: Value
4921
+ done?: boolean
4922
+ }
4923
+
4924
+ export interface FramerMotionGenerator<Value = unknown> {
4925
+ calculatedDuration?: number | null
4926
+ next(time: number): FramerMotionGeneratorState<Value>
4927
+ velocity?(time: number): number
4928
+ }
4929
+
4930
+ export interface FramerMotionAnimationDriver {
4931
+ now(): number
4932
+ start(autoplay?: boolean): void
4933
+ stop(): void
4934
+ }
4935
+
4936
+ export interface FramerMotionAnimationOptions<Value = unknown> {
4937
+ keyframes: Value[]
4938
+ autoplay?: boolean
4939
+ delay?: number
4940
+ repeat?: number
4941
+ repeatDelay?: number
4942
+ repeatType?: 'loop' | 'reverse' | 'mirror'
4943
+ velocity?: number
4944
+ finalKeyframe?: Value
4945
+ startTime?: number
4946
+ allowFlatten?: boolean
4947
+ motionValue?: { updatedAt?: number }
4948
+ type?: (options: FramerMotionAnimationOptions<Value>) => FramerMotionGenerator<Value>
4949
+ driver?: (update: (timestamp: number) => void) => FramerMotionAnimationDriver
4950
+ onUpdate?(latest: Value): void
4951
+ onPlay?(): void
4952
+ onComplete?(): void
4953
+ onCancel?(): void
4954
+ onStop?(): void
4955
+ }
4956
+
4957
+ export declare class JSAnimation<Value = unknown> implements PromiseLike<void> {
4958
+ constructor(options: FramerMotionAnimationOptions<Value>)
4959
+ options: FramerMotionAnimationOptions<Value>
4960
+ state: 'idle' | 'running' | 'paused' | 'finished'
4961
+ currentTime: number
4962
+ playbackSpeed: number
4963
+ readonly duration: number
4964
+ readonly iterationDuration: number
4965
+ time: number
4966
+ speed: number
4967
+ readonly finished: Promise<void>
4968
+ then<TResult1 = void, TResult2 = never>(
4969
+ onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
4970
+ onrejected?: ((reason: Error) => TResult2 | PromiseLike<TResult2>) | null,
4971
+ ): Promise<TResult1 | TResult2>
4972
+ play(): void
4973
+ pause(): void
4974
+ complete(): void
4975
+ finish(): void
4976
+ cancel(): void
4977
+ stop(): void
4978
+ tick(timestamp: number, sample?: boolean): FramerMotionGeneratorState<Value>
4979
+ sample(sampleTime: number): FramerMotionGeneratorState<Value>
4980
+ attachTimeline(timeline: { observe(animation: JSAnimation<Value>): VoidFunction }): VoidFunction
4981
+ }
4982
+
4983
+ export declare const MotionGlobalConfig: FramerMotionGlobalConfig
4984
+ export declare const frameData: FramerMotionFrameData
4985
+ export declare const frameSteps: FramerMotionFrameSteps
4986
+ export declare const time: FramerMotionTime
4987
+ export declare const visualElementStore: WeakMap<object, unknown>
4988
+ export declare function _injectRuntime(injectedRuntime: FramerInjectedRuntime): void
4989
+
4862
4990
  export * from 'real-framer-motion'