unframer 2.27.2 → 3.0.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.
Files changed (118) hide show
  1. package/dist/babel-jsx.js +2 -2
  2. package/dist/babel-jsx.js.map +1 -1
  3. package/dist/babel-typedoc.d.ts +39 -0
  4. package/dist/babel-typedoc.d.ts.map +1 -0
  5. package/dist/babel-typedoc.js +77 -0
  6. package/dist/babel-typedoc.js.map +1 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +7 -2
  9. package/dist/cli.js.map +1 -1
  10. package/dist/esbuild.d.ts +2 -1
  11. package/dist/esbuild.d.ts.map +1 -1
  12. package/dist/esbuild.js +16 -9
  13. package/dist/esbuild.js.map +1 -1
  14. package/dist/exporter.d.ts +25 -8
  15. package/dist/exporter.d.ts.map +1 -1
  16. package/dist/exporter.js +381 -195
  17. package/dist/exporter.js.map +1 -1
  18. package/dist/exporter.test.js +0 -4
  19. package/dist/exporter.test.js.map +1 -1
  20. package/dist/framer.js +229 -102
  21. package/dist/generated/api-client.d.ts +3 -3
  22. package/dist/generated/api-client.d.ts.map +1 -1
  23. package/dist/package-manager.d.ts +10 -0
  24. package/dist/package-manager.d.ts.map +1 -0
  25. package/dist/package-manager.js +145 -0
  26. package/dist/package-manager.js.map +1 -0
  27. package/dist/react.d.ts +32 -0
  28. package/dist/react.d.ts.map +1 -1
  29. package/dist/react.js +1 -3
  30. package/dist/react.js.map +1 -1
  31. package/dist/undici-dispatcher.js +1 -2
  32. package/dist/undici-dispatcher.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.d.ts.map +1 -1
  35. package/dist/version.js +1 -1
  36. package/dist/version.js.map +1 -1
  37. package/esm/babel-jsx.js +2 -2
  38. package/esm/babel-jsx.js.map +1 -1
  39. package/esm/babel-typedoc.d.ts +39 -0
  40. package/esm/babel-typedoc.d.ts.map +1 -0
  41. package/esm/babel-typedoc.js +74 -0
  42. package/esm/babel-typedoc.js.map +1 -0
  43. package/esm/cli.d.ts.map +1 -1
  44. package/esm/cli.js +7 -2
  45. package/esm/cli.js.map +1 -1
  46. package/esm/esbuild.d.ts +2 -1
  47. package/esm/esbuild.d.ts.map +1 -1
  48. package/esm/esbuild.js +16 -9
  49. package/esm/esbuild.js.map +1 -1
  50. package/esm/exporter.d.ts +25 -8
  51. package/esm/exporter.d.ts.map +1 -1
  52. package/esm/exporter.js +378 -194
  53. package/esm/exporter.js.map +1 -1
  54. package/esm/exporter.test.js +0 -4
  55. package/esm/exporter.test.js.map +1 -1
  56. package/esm/framer.js +229 -102
  57. package/esm/package-manager.d.ts +10 -0
  58. package/esm/package-manager.d.ts.map +1 -0
  59. package/esm/package-manager.js +141 -0
  60. package/esm/package-manager.js.map +1 -0
  61. package/esm/react.d.ts +32 -0
  62. package/esm/react.d.ts.map +1 -1
  63. package/esm/react.js +1 -3
  64. package/esm/react.js.map +1 -1
  65. package/esm/undici-dispatcher.js +1 -2
  66. package/esm/undici-dispatcher.js.map +1 -1
  67. package/esm/version.d.ts +1 -1
  68. package/esm/version.d.ts.map +1 -1
  69. package/esm/version.js +1 -1
  70. package/esm/version.js.map +1 -1
  71. package/package.json +5 -4
  72. package/src/babel-jsx.ts +2 -2
  73. package/src/babel-typedoc.ts +132 -0
  74. package/src/cli.ts +7 -2
  75. package/src/esbuild.ts +17 -12
  76. package/src/exporter.test.ts +0 -5
  77. package/src/exporter.ts +448 -237
  78. package/src/framer.js +237 -103
  79. package/src/package-manager.ts +164 -0
  80. package/src/react.tsx +33 -0
  81. package/src/undici-dispatcher.ts +1 -1
  82. package/src/version.ts +1 -1
  83. package/dist/framer.d.ts.map +0 -1
  84. package/dist/framer.js.map +0 -1
  85. package/esm/framer-chunks/chunk-22NYTOTD.d.ts +0 -14
  86. package/esm/framer-chunks/chunk-22NYTOTD.d.ts.map +0 -1
  87. package/esm/framer-chunks/chunk-22NYTOTD.js +0 -99
  88. package/esm/framer-chunks/chunk-22NYTOTD.js.map +0 -1
  89. package/esm/framer-chunks/fontshare-GSJIWLGZ-7BHTUG6K.d.ts +0 -115
  90. package/esm/framer-chunks/fontshare-GSJIWLGZ-7BHTUG6K.d.ts.map +0 -1
  91. package/esm/framer-chunks/fontshare-GSJIWLGZ-7BHTUG6K.js +0 -5
  92. package/esm/framer-chunks/fontshare-GSJIWLGZ-7BHTUG6K.js.map +0 -1
  93. package/esm/framer-chunks/fontshare-SSHBFVID-ZX5Y6FJ4.d.ts +0 -781
  94. package/esm/framer-chunks/fontshare-SSHBFVID-ZX5Y6FJ4.d.ts.map +0 -1
  95. package/esm/framer-chunks/fontshare-SSHBFVID-ZX5Y6FJ4.js +0 -5
  96. package/esm/framer-chunks/fontshare-SSHBFVID-ZX5Y6FJ4.js.map +0 -1
  97. package/esm/framer-chunks/fontshare-X6MCIXW5-FUMOBUA2.d.ts +0 -634
  98. package/esm/framer-chunks/fontshare-X6MCIXW5-FUMOBUA2.d.ts.map +0 -1
  99. package/esm/framer-chunks/fontshare-X6MCIXW5-FUMOBUA2.js +0 -5
  100. package/esm/framer-chunks/fontshare-X6MCIXW5-FUMOBUA2.js.map +0 -1
  101. package/esm/framer-chunks/framer-font-TNC5DMGA-XVG7BST3.d.ts +0 -18
  102. package/esm/framer-chunks/framer-font-TNC5DMGA-XVG7BST3.d.ts.map +0 -1
  103. package/esm/framer-chunks/framer-font-TNC5DMGA-XVG7BST3.js +0 -5
  104. package/esm/framer-chunks/framer-font-TNC5DMGA-XVG7BST3.js.map +0 -1
  105. package/esm/framer-chunks/google-3GQMHAEU-KEOTHDV6.d.ts +0 -9827
  106. package/esm/framer-chunks/google-3GQMHAEU-KEOTHDV6.d.ts.map +0 -1
  107. package/esm/framer-chunks/google-3GQMHAEU-KEOTHDV6.js +0 -5
  108. package/esm/framer-chunks/google-3GQMHAEU-KEOTHDV6.js.map +0 -1
  109. package/esm/framer-chunks/google-42BCYVR5-PDCHFNPY.d.ts +0 -3231
  110. package/esm/framer-chunks/google-42BCYVR5-PDCHFNPY.d.ts.map +0 -1
  111. package/esm/framer-chunks/google-42BCYVR5-PDCHFNPY.js +0 -5
  112. package/esm/framer-chunks/google-42BCYVR5-PDCHFNPY.js.map +0 -1
  113. package/esm/framer-chunks/google-LHIHIYDX-FZZ6UXE7.d.ts +0 -1499
  114. package/esm/framer-chunks/google-LHIHIYDX-FZZ6UXE7.d.ts.map +0 -1
  115. package/esm/framer-chunks/google-LHIHIYDX-FZZ6UXE7.js +0 -5
  116. package/esm/framer-chunks/google-LHIHIYDX-FZZ6UXE7.js.map +0 -1
  117. package/esm/framer.d.ts.map +0 -1
  118. package/esm/framer.js.map +0 -1
package/src/exporter.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  babelPluginJsxTransform,
20
20
  removeJsxExpressionContainer,
21
21
  } from './babel-jsx.js'
22
+ import { babelPluginTypedoc } from './babel-typedoc.js'
22
23
  import { propCamelCaseJustLikeFramer } from './compat.js'
23
24
  import {
24
25
  ComponentFontBundle,
@@ -48,6 +49,8 @@ import {
48
49
  stackblitzDemoExample,
49
50
  terminalMarkdown,
50
51
  } from './utils.js'
52
+ import { installPackagesBatch } from './package-manager.js'
53
+ import { version as currentUnframerVersion } from './version.js'
51
54
 
52
55
  import { Biome, Distribution } from '@biomejs/js-api'
53
56
 
@@ -78,6 +81,27 @@ export async function bundle({
78
81
  await fs.promises.mkdir(out, { recursive: true })
79
82
  } catch (e) {}
80
83
 
84
+ // Prefix for temporary .js files to avoid HMR issues
85
+ const tempJsPrefix = 'temp_'
86
+
87
+ // Helper function to handle file path transformations with temp prefix
88
+ function getFilePaths(filePath: string, outDir: string) {
89
+ const baseName = path.basename(filePath)
90
+ const dirName = path.dirname(filePath)
91
+ const tempFileName = tempJsPrefix + baseName
92
+ const tempFilePath = path.join(dirName, tempFileName)
93
+
94
+ return {
95
+ originalPath: filePath,
96
+ tempJsPath: path.resolve(outDir, tempFilePath),
97
+ finalJsPath: path.resolve(outDir, filePath),
98
+ jsxPath: path.resolve(outDir, filePath.replace(/\.js$/, '.jsx')),
99
+ tempFilePath,
100
+ baseName,
101
+ dirName,
102
+ }
103
+ }
104
+
81
105
  spinner.start('exporting components...')
82
106
 
83
107
  const otherRoutes = Object.fromEntries(
@@ -100,7 +124,7 @@ export async function bundle({
100
124
  }
101
125
  }
102
126
  const fn = watch ? context : fakeContext
103
- let foundError = false
127
+ const missingPackages = new Set<string>()
104
128
 
105
129
  const buildContext = await fn({
106
130
  absWorkingDir: out,
@@ -130,9 +154,12 @@ export async function bundle({
130
154
  signal,
131
155
  externalPackages: config.externalPackages,
132
156
  externalizeNpm: config.allExternal,
133
- outDir: config.outDir,
157
+ outDir: out,
134
158
  onMissingPackage: (e) => {
135
- foundError = true
159
+ // No longer needed - packages are auto-installed
160
+ },
161
+ onCollectMissingPackage: (pkg) => {
162
+ missingPackages.add(pkg)
136
163
  },
137
164
  }),
138
165
  nodeModulesPolyfillPlugin({}),
@@ -150,6 +177,15 @@ export async function bundle({
150
177
  { filter: /.*/, namespace: 'virtual' },
151
178
  async (args) => {
152
179
  const name = args.path
180
+
181
+ // Handle virtual routes module
182
+ if (name === '__routes') {
183
+ return {
184
+ contents: `export const routes = ${JSON.stringify(otherRoutes, null, 2)};`,
185
+ loader: 'js',
186
+ }
187
+ }
188
+
153
189
  const url = components[name]
154
190
  const componentBreakpoints =
155
191
  config.componentBreakpoints?.filter(
@@ -184,6 +220,9 @@ export async function bundle({
184
220
  ])
185
221
  : {}
186
222
 
223
+ // Use virtual routes module
224
+ const routesImportPath = 'virtual:__routes'
225
+
187
226
  return {
188
227
  contents: /** js **/ `
189
228
  'use client'
@@ -194,6 +233,7 @@ export async function bundle({
194
233
  signal,
195
234
  })}'
196
235
  import { WithFramerBreakpoints } from 'unframer'
236
+ import { routes } from '${routesImportPath}'
197
237
  const locales = ${
198
238
  JSON.stringify(config.locales) || '[]'
199
239
  }
@@ -203,17 +243,12 @@ export async function bundle({
203
243
  2,
204
244
  )}
205
245
 
206
- Component.Responsive = ({ locale, ...rest }) => {
246
+
247
+ function ComponentWithRoot({ locale, ...rest }) {
207
248
  return (
208
249
  <ContextProviders
209
- routes={${JSON.stringify(
210
- otherRoutes,
211
- )}}
212
- children={<WithFramerBreakpoints
213
- Component={Component}
214
- variants={defaultResponsiveVariants}
215
- {...rest}
216
- />}
250
+ routes={routes}
251
+ children={<Component {...rest} />}
217
252
  framerSiteId={${JSON.stringify(
218
253
  config.fullFramerProjectId,
219
254
  )}}
@@ -222,16 +257,15 @@ export async function bundle({
222
257
  />
223
258
  )
224
259
  }
225
-
226
- export default function ComponentWithRoot({ locale, ...rest }) {
260
+ ComponentWithRoot.Responsive = ({ locale, ...rest }) => {
227
261
  return (
228
262
  <ContextProviders
229
- routes={${JSON.stringify(
230
- otherRoutes,
231
- null,
232
- 2,
233
- )}}
234
- children={<Component {...rest} />}
263
+ routes={routes}
264
+ children={<WithFramerBreakpoints
265
+ Component={Component}
266
+ variants={defaultResponsiveVariants}
267
+ {...rest}
268
+ />}
235
269
  framerSiteId={${JSON.stringify(
236
270
  config.fullFramerProjectId,
237
271
  )}}
@@ -241,6 +275,7 @@ export async function bundle({
241
275
  )
242
276
  }
243
277
  Object.assign(ComponentWithRoot, Component)
278
+ export default ComponentWithRoot
244
279
  `,
245
280
  loader: 'jsx',
246
281
  }
@@ -258,10 +293,35 @@ export async function bundle({
258
293
  } "${config.projectName}", do not edit manually */\n`
259
294
 
260
295
  async function rebuild() {
296
+ // Clear missing packages for each rebuild (important for watch mode)
297
+ missingPackages.clear()
298
+ try {
299
+ const installedVersion = await resolvePackageVersion({
300
+ cwd: out,
301
+ pkg: 'unframer',
302
+ })
303
+ if (
304
+ isVersionGreater(
305
+ installedVersion || '0.0.0',
306
+ currentUnframerVersion || '0.0.0',
307
+ )
308
+ ) {
309
+ // Version mismatch, add with specific version
310
+ missingPackages.add(`unframer@${currentUnframerVersion}`)
311
+ spinner.info(
312
+ `Different unframer version detected (${installedVersion}), will install unframer@${currentUnframerVersion}`,
313
+ )
314
+ }
315
+ } catch (e) {
316
+ // Unframer not installed, add with specific version
317
+ missingPackages.add(`unframer@${currentUnframerVersion}`)
318
+ spinner.info(
319
+ `Missing package detected: unframer@${currentUnframerVersion}`,
320
+ )
321
+ }
261
322
  const prevFiles = await recursiveReaddir(out)
262
323
  const buildResult = await buildContext.rebuild().catch((e) => {
263
324
  if (e.message.includes('No matching export ')) {
264
- foundError = true
265
325
  spinner.error(
266
326
  `esbuild failed to import from an external package, this usually means that the npm package version in Framer is older than the latest.`,
267
327
  )
@@ -275,117 +335,43 @@ export async function bundle({
275
335
 
276
336
  spinner.update('Finished build')
277
337
 
278
- for (let file of buildResult.outputFiles!) {
279
- const resultPathAbsJs = path.resolve(out, file.path)
280
- const resultPathAbsJsx = resultPathAbsJs.replace(/\.js$/, '.jsx')
281
-
282
- const existing = await fs.promises
283
- .readFile(resultPathAbsJsx, 'utf-8')
284
- .catch(() => null)
285
- const tooBigSize = 0.7 * 1024 * 1024
338
+ // Install missing packages if any were collected
339
+ if (missingPackages.size > 0) {
340
+ const packagesToInstall = Array.from(missingPackages)
341
+ logger.log(
342
+ `Installing missing packages: ${packagesToInstall.join(', ')}`,
343
+ )
286
344
 
287
- let formatted = file.text
345
+ const installResult = await installPackagesBatch({
346
+ packageNames: packagesToInstall,
347
+ cwd: out,
348
+ isDev: false,
349
+ })
288
350
 
289
- let tooBig = file.text.length >= tooBigSize
290
- let didFormat = false
291
- if (
292
- config.jsx &&
293
- !tooBig &&
294
- !resultPathAbsJs.includes('/chunks/') &&
295
- !resultPathAbsJs.includes('\\chunks\\')
296
- ) {
297
- try {
298
- let res = transform(file.text || '', {
299
- babelrc: false,
300
- sourceType: 'module',
301
- plugins: [
302
- // babelPluginDeduplicateImports,
303
- babelPluginJsxTransform,
304
- removeJsxExpressionContainer,
305
- ],
306
- // ast: true,
307
- // code: false,
308
- filename: 'x.jsx',
309
- compact: false,
310
- sourceMaps: false,
311
- })
312
- if (res?.code) {
313
- if (!biome) {
314
- biome = await Biome.create({
315
- distribution: Distribution.NODE,
316
- })
317
- }
318
- let result = biome.formatContent(res.code, {
319
- filePath: 'example.jsx',
320
- })
321
- didFormat = true
322
- formatted = result.content
323
- }
324
- } catch (e) {
325
- notifyError(e, 'babel transform and format')
326
- }
351
+ if (!installResult.success) {
352
+ spinner.error(
353
+ `Failed to install packages: ${installResult.error}`,
354
+ )
355
+ // Don't fail the build, just warn
327
356
  }
357
+ }
328
358
 
329
- // let inputCode = res!.code!
330
- // let shouldFormat = !tooBig && !file.path.includes('chunks')
331
- // if (shouldFormat) {
332
- // spinner.update(`Formatting ${path.relative(out, file.path)}`)
333
- // formatted = dprint.format('file.jsx', file.text, {
334
- // lineWidth: 140,
335
- // quoteStyle: 'alwaysSingle',
336
- // trailingCommas: 'always',
337
- // semiColons: 'always',
338
- // })
339
- // }
340
- // if (tooBig) {
341
- // spinner.info(
342
- // `skipping formatting ${path.relative(
343
- // out,
344
- // file.path,
345
- // )}, too big`,
346
- // )
347
- // }
348
-
349
- // if (framerWebPages?.length) {
350
- // codeNew = replaceWebPageIds({
351
- // code: codeNew,
352
- // elements: framerWebPages,
353
- // })
354
- // }
355
- // const lines = findRelativeLinks(codeNew)
356
- // if (lines.length) {
357
- // spinner.error(
358
- // `found broken links for ${path.relative(out, file.path)}`,
359
- // )
360
- // lines.forEach((line) => {
361
- // logger.log(`${path.resolve(out, file.path)}:${line + 1}`)
362
- // })
363
- // }
364
-
359
+ // First, write raw JS files for type extraction with temp prefix
360
+ for (let file of buildResult.outputFiles!) {
361
+ const paths = getFilePaths(file.path, out)
365
362
  const prefix =
366
363
  `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment
367
- const codeJsx = prefix + formatted
368
364
  const codeJs = prefix + file.text
369
- // if (existing === codeJsx) {
370
- // continue
371
- // }
372
- logger.log(`writing`, path.relative(out, file.path))
373
- await fs.promises.mkdir(path.dirname(resultPathAbsJsx), {
365
+
366
+ logger.log(
367
+ `writing temp JS`,
368
+ path.relative(out, paths.tempFilePath),
369
+ )
370
+ await fs.promises.mkdir(path.dirname(paths.tempJsPath), {
374
371
  recursive: true,
375
372
  })
376
- if (codeJs !== codeJsx || !didFormat) {
377
- await fs.promises.writeFile(resultPathAbsJs, codeJs, 'utf-8')
378
- }
379
- if (didFormat) {
380
- await fs.promises.writeFile(resultPathAbsJsx, codeJsx, 'utf-8')
381
- }
373
+ await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8')
382
374
  }
383
- spinner.stop()
384
- await fs.promises.writeFile(
385
- path.resolve(out, '.cursorignore'),
386
- `**/*.js\nchunks\n`,
387
- 'utf-8',
388
- )
389
375
 
390
376
  if (!buildResult?.outputFiles) {
391
377
  throw new Error('Failed to generate result')
@@ -408,7 +394,8 @@ export async function bundle({
408
394
  const name = path
409
395
  .relative(out, file.path)
410
396
  .replace(/\.jsx?$/, '')
411
- const resultPathAbs = path.resolve(out, file.path)
397
+ const paths = getFilePaths(file.path, out)
398
+ const resultPathAbs = paths.tempJsPath
412
399
  if (!components[name]) {
413
400
  return
414
401
  }
@@ -419,6 +406,7 @@ export async function bundle({
419
406
  return
420
407
  }
421
408
  logger.log(`extracting types for ${name}`)
409
+ spinner.info(`Extracting types for component: ${name}`)
422
410
  spinner.update(`Extracting types for ${name}`)
423
411
  const { propertyControls, fonts } =
424
412
  await extractPropControlsUnsafe(resultPathAbs, name)
@@ -432,21 +420,23 @@ export async function bundle({
432
420
  fileName: path.basename(file.path),
433
421
  })),
434
422
  )
435
- const types = propControlsToType({
423
+ const typedocComments = propControlsToTypedocComments({
436
424
  controls: propertyControls!,
437
425
  fileName: name,
438
426
  config,
439
427
  })
440
- await fs.promises.mkdir(out, { recursive: true })
441
- await fs.promises.writeFile(
442
- path.resolve(out, `${name}.d.ts`),
443
- types,
428
+ logger.log(
429
+ `Generated TypeDoc comments for ${name}: ${!!typedocComments.headerComment}`,
444
430
  )
431
+ await fs.promises.mkdir(out, { recursive: true })
432
+ // .d.ts generation removed – types are now injected as typedoc
433
+ // comments directly inside the generated JSX file.
445
434
 
446
435
  return {
447
436
  propertyControls,
448
437
  fonts,
449
438
  name,
439
+ typedocComments,
450
440
  }
451
441
  } finally {
452
442
  sema.release()
@@ -459,7 +449,6 @@ export async function bundle({
459
449
  // Ignore error if file doesn't exist or can't be deleted
460
450
  }
461
451
  })
462
- // spinner.stop()
463
452
 
464
453
  const cssString =
465
454
  doNotEditComment +
@@ -487,11 +476,18 @@ export async function bundle({
487
476
  .filter(
488
477
  (x) =>
489
478
  x.path.endsWith('.js') &&
490
- fs.existsSync(x.path.replace(/\.js$/, '.jsx')),
479
+ fs.existsSync(getFilePaths(x.path, out).jsxPath),
491
480
  )
492
- .map((x) => x.path.replace(/\.js$/, '.jsx'))
481
+ .map((x) => getFilePaths(x.path, out).jsxPath)
493
482
  const outFiles = buildResult.outputFiles
494
- .map((x) => path.resolve(out, x.path))
483
+ .map((x) => {
484
+ const paths = getFilePaths(x.path, out)
485
+ if (x.path.endsWith('.js') && fs.existsSync(paths.jsxPath)) {
486
+ return null // Will be handled by jsx files
487
+ }
488
+ return paths.finalJsPath
489
+ })
490
+ .filter(Boolean)
495
491
  .concat([
496
492
  path.resolve(out, 'meta.json'),
497
493
  path.resolve(out, 'tokens.css'),
@@ -499,23 +495,10 @@ export async function bundle({
499
495
  path.resolve(out, 'styles.css'),
500
496
  ])
501
497
  .concat(jsxFiles)
502
- .concat(
503
- buildResult.outputFiles.map((x) =>
504
- path.resolve(out, x.path.replace(/\.jsx?$/, '.d.ts')),
505
- ),
506
- )
507
498
 
508
499
  const filesToDelete = prevFiles
509
500
  .filter((x) => !outFiles.includes(x))
510
- .concat(
511
- buildResult.outputFiles
512
- .map((x) => x.path)
513
- .filter(
514
- (js) =>
515
- js.endsWith('.js') &&
516
- jsxFiles.some((x) => x.startsWith(js)),
517
- ),
518
- )
501
+ .filter((x) => !x.includes(tempJsPrefix)) // Don't delete temp files here, they're handled separately
519
502
 
520
503
  for (let file of filesToDelete) {
521
504
  logger.log('deleting', path.relative(out, file))
@@ -539,17 +522,7 @@ export async function bundle({
539
522
  if (watch) {
540
523
  logger.log('waiting for components or config changes')
541
524
  }
542
- if (!tokens?.length) {
543
- const tokensCss =
544
- "/* This css file contains your color variables, sometimes these get desynced when updated in Framer so it's good that you copy and paste this snippet into your app css */\n" +
545
- '/* Bug: https://www.framer.community/c/bugs/color-style-unlinks-when-copying-component-between-projects-resulting-in-potential-value-discrepancy */\n' +
546
- getTokensCss({ out, result: buildResult })
547
- await fs.promises.writeFile(
548
- path.resolve(out, 'tokens.css'),
549
- tokensCss,
550
- 'utf-8',
551
- )
552
- }
525
+
553
526
  const res: BundleResult = {
554
527
  components: Object.entries(components).map(([name, v]) => {
555
528
  const propControls = propControlsData.find(
@@ -566,6 +539,160 @@ export async function bundle({
566
539
  }),
567
540
  }
568
541
 
542
+ // Process and write JSX files with TypeDoc comments
543
+ spinner.update('Processing JSX files with TypeDoc comments')
544
+ for (let file of buildResult.outputFiles!) {
545
+ const paths = getFilePaths(file.path, out)
546
+
547
+ const componentName = path
548
+ .relative(out, file.path)
549
+ .replace(/\.js$/, '')
550
+ const propData = propControlsData.find(
551
+ (p) => p?.name === componentName,
552
+ )
553
+ const typedocComments = propData?.typedocComments
554
+
555
+ logger.log(`Processing component: ${componentName}`)
556
+ spinner.update(`Processing JSX for ${componentName}`)
557
+ if (!propData) {
558
+ logger.log(` No propData found for ${componentName}`)
559
+ } else {
560
+ logger.log(
561
+ ` PropData found for ${componentName}, has propertyControls: ${!!propData.propertyControls}`,
562
+ )
563
+ if (!typedocComments) {
564
+ logger.log(` No typedocComments for ${componentName}`)
565
+ } else {
566
+ logger.log(
567
+ ` TypeDoc comments available for ${componentName}`,
568
+ )
569
+ }
570
+ }
571
+
572
+ const existing = await fs.promises
573
+ .readFile(paths.jsxPath, 'utf-8')
574
+ .catch(() => null)
575
+ const tooBigSize = 0.7 * 1024 * 1024
576
+
577
+ let formatted = file.text
578
+
579
+ let tooBig = file.text.length >= tooBigSize
580
+ let didFormat = false
581
+ if (
582
+ config.jsx &&
583
+ !tooBig &&
584
+ !paths.tempJsPath.includes('/chunks/') &&
585
+ !paths.tempJsPath.includes('\\chunks\\')
586
+ ) {
587
+ try {
588
+ const plugins = [
589
+ // babelPluginDeduplicateImports,
590
+ babelPluginJsxTransform,
591
+ removeJsxExpressionContainer,
592
+ ]
593
+
594
+ // Add TypeDoc plugin if we have comments for this component
595
+ if (typedocComments) {
596
+ logger.log(
597
+ ` Adding TypeDoc plugin for ${componentName}`,
598
+ )
599
+ plugins.push(babelPluginTypedoc(typedocComments))
600
+ } else {
601
+ logger.log(
602
+ ` No TypeDoc comments to add for ${componentName}`,
603
+ )
604
+ }
605
+
606
+ let res = transform(file.text || '', {
607
+ babelrc: false,
608
+ sourceType: 'module',
609
+ parserOpts: {
610
+ plugins: ['jsx'],
611
+ },
612
+ plugins,
613
+ // ast: true,
614
+ // code: false,
615
+ filename: 'x.jsx',
616
+ compact: false,
617
+ sourceMaps: false,
618
+ })
619
+ if (res?.code) {
620
+ if (!biome) {
621
+ biome = await Biome.create({
622
+ distribution: Distribution.NODE,
623
+ })
624
+ }
625
+ let result = biome.formatContent(res.code, {
626
+ filePath: 'example.jsx',
627
+ })
628
+ didFormat = true
629
+ formatted = result.content
630
+ }
631
+ } catch (e) {
632
+ notifyError(e, 'babel transform and format')
633
+ }
634
+ }
635
+
636
+ const prefix =
637
+ `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment
638
+ const codeJsx = prefix + formatted
639
+ const codeJs = prefix + file.text
640
+ logger.log(`writing`, path.relative(out, file.path))
641
+ await fs.promises.mkdir(path.dirname(paths.jsxPath), {
642
+ recursive: true,
643
+ })
644
+ // Always write the temp .js file for type extraction
645
+ await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8')
646
+
647
+ // Only write .jsx file if it's different from existing or if formatting was done
648
+ if (didFormat && codeJsx !== existing) {
649
+ await fs.promises.writeFile(paths.jsxPath, codeJsx, 'utf-8')
650
+ }
651
+ }
652
+ spinner.stop()
653
+ // await fs.promises.writeFile(
654
+ // path.resolve(out, '.cursorignore'),
655
+ // `**/*.js\nchunks\n`,
656
+ // 'utf-8',
657
+ // )
658
+
659
+ // Clean up temp .js files and handle prefixes
660
+ for (let file of buildResult.outputFiles!) {
661
+ if (file.path.endsWith('.js')) {
662
+ const paths = getFilePaths(file.path, out)
663
+
664
+ if (fs.existsSync(paths.jsxPath)) {
665
+ // Remove temp .js file if .jsx equivalent exists
666
+ logger.log(
667
+ 'removing temp JS file with JSX equivalent:',
668
+ path.relative(out, paths.tempJsPath),
669
+ )
670
+ try {
671
+ await fs.promises.rm(paths.tempJsPath)
672
+ await fs.promises.rm(paths.finalJsPath)
673
+ } catch (error) {
674
+ // Ignore error if file doesn't exist
675
+ }
676
+ } else {
677
+ // Rename temp .js file to final name if no .jsx equivalent
678
+ logger.log(
679
+ 'renaming temp JS file to final name:',
680
+ path.relative(out, paths.tempJsPath),
681
+ '->',
682
+ path.relative(out, paths.finalJsPath),
683
+ )
684
+ try {
685
+ await fs.promises.rename(
686
+ paths.tempJsPath,
687
+ paths.finalJsPath,
688
+ )
689
+ } catch (error) {
690
+ // Ignore error if file doesn't exist
691
+ }
692
+ }
693
+ }
694
+ }
695
+
569
696
  spinner.info(`Build completed`)
570
697
  return res
571
698
  }
@@ -594,8 +721,11 @@ export async function bundle({
594
721
  console.log()
595
722
  console.log()
596
723
  const outDirForExample =
597
- path.posix.relative(process.cwd(), out).replace(/^src\//, '') ||
598
- 'framer' // remove src so file works inside src
724
+ path
725
+ .relative(process.cwd(), out)
726
+ .split(path.sep)
727
+ .join('/')
728
+ .replace(/^src\//, '') || 'framer' // remove src so file works inside src
599
729
  const { exampleCode } = await createExampleComponentCode({
600
730
  outDir: out,
601
731
  // buildResult: result,
@@ -608,42 +738,35 @@ export async function bundle({
608
738
  })
609
739
  await fs.promises.writeFile(stackblitzDemoExample, exampleCode)
610
740
  }
611
- if (!foundError) {
612
- console.log(
613
- terminalMarkdown(dedent`
614
- # How to use the Framer components
615
-
616
- Your components are exported to \`${outDirForExample}\` folder. Now please install the \`unframer\` runtime dependency:
741
+ console.log(
742
+ terminalMarkdown(dedent`
743
+ # How to use the Framer components
617
744
 
618
- \`\`\`sh
619
- npm install unframer
620
- \`\`\`
745
+ Your components are exported to \`${outDirForExample}\` folder.
621
746
 
622
- Each component has a \`.Responsive\` variant that allows you to specify different variants for different breakpoints.
747
+ Each component has a \`.Responsive\` variant that allows you to specify different variants for different breakpoints.
623
748
 
624
- You can use the components like this (try copy pasting the code below into your React app):
749
+ You can use the components like this (try copy pasting the code below into your React app):
625
750
 
626
- \`\`\`jsx
627
- ${exampleCode}
628
- \`\`\`
751
+ \`\`\`jsx
752
+ ${exampleCode}
753
+ \`\`\`
629
754
 
630
- It's very important to import the \`styles.css\` file to include the necessary styles for the components.
755
+ It's very important to import the \`styles.css\` file to include the necessary styles for the components.
631
756
 
632
- To style components you can pass a \`style\` or \`className\` prop (but remember to use !important to increase the specificity).
757
+ To style components you can pass a \`style\` or \`className\` prop (but remember to use !important to increase the specificity).
633
758
 
634
- Read more on GitHub: https://github.com/remorses/unframer
759
+ Read more on GitHub: https://github.com/remorses/unframer
635
760
 
636
- `),
637
- )
638
- }
639
- await checkUnframerVersion({ cwd: out })
761
+ `),
762
+ )
640
763
  console.log()
641
764
  return { result, rebuild, buildContext }
642
765
  }
643
766
 
644
767
  const packageVersionCache = new Map<string, string>()
645
768
 
646
- export function resolvePackage({ cwd, pkg }) {
769
+ export function resolvePackageVersion({ cwd, pkg }) {
647
770
  if (packageVersionCache.has(pkg)) {
648
771
  return Promise.resolve(packageVersionCache.get(pkg))
649
772
  }
@@ -664,10 +787,8 @@ export function resolvePackage({ cwd, pkg }) {
664
787
  },
665
788
  (error, stdout, stderr) => {
666
789
  if (error) {
667
- logger.log(stderr)
668
- reject(
669
- new Error(`${pkg} is not installed in your project`),
670
- )
790
+ // Package not installed - this is expected and handled by auto-install
791
+ reject(new Error(`${pkg} is not installed in your project`))
671
792
  return
672
793
  }
673
794
  const version = stdout.trim()
@@ -678,20 +799,31 @@ export function resolvePackage({ cwd, pkg }) {
678
799
  })
679
800
  }
680
801
 
681
- export async function checkUnframerVersion({ cwd }: { cwd: string }) {
682
- const currentVersion = packageJson.version
683
- try {
684
- const installedVersion = await resolvePackage({ cwd, pkg: 'unframer' })
685
- if (installedVersion !== currentVersion) {
686
- spinner.error(
687
- `IMPORTANT: Unframer version mismatch. Please run: npm update unframer@latest`,
688
- )
689
- }
690
- } catch (e) {
691
- spinner.error(
692
- 'IMPORTANT: Unframer is not installed in your project. Please run: npm install unframer',
802
+ export function resolvePackage({ cwd, pkg }) {
803
+ return new Promise<boolean>((resolve) => {
804
+ const code = `import('${pkg}/package.json', { with: { type: 'json' } }).then(()=>console.log('true')).catch(()=>import('${pkg}').then(()=>console.log('true')).catch(()=>console.log('false')));`
805
+
806
+ const command = [
807
+ JSON.stringify(nodePath),
808
+ '-e',
809
+ JSON.stringify(code),
810
+ ].join(' ')
811
+
812
+ exec(
813
+ command,
814
+ {
815
+ cwd,
816
+ },
817
+ (error, stdout) => {
818
+ if (error) {
819
+ resolve(false)
820
+ return
821
+ }
822
+ const exists = stdout.trim().split('\n').pop() === 'true'
823
+ resolve(exists)
824
+ },
693
825
  )
694
- }
826
+ })
695
827
  }
696
828
 
697
829
  export function getDarkModeSelector(opts: {
@@ -820,7 +952,7 @@ async function extractPropControlsSafe(text, name) {
820
952
  }
821
953
  }
822
954
 
823
- function getTokensCss({
955
+ async function getTokensCss({
824
956
  out,
825
957
  result,
826
958
  }: {
@@ -1007,14 +1139,18 @@ function safeJsonParse(text) {
1007
1139
  }
1008
1140
  }
1009
1141
 
1010
- export function propControlsToType({
1142
+ /**
1143
+ * Generates TypeDoc comments that will be injected into JSX files
1144
+ * instead of generating separate .d.ts files
1145
+ */
1146
+ export function propControlsToTypedocComments({
1011
1147
  config,
1012
1148
  fileName,
1013
1149
  controls,
1014
1150
  }: {
1015
1151
  controls: PropertyControls
1016
- fileName
1017
- config
1152
+ fileName: string
1153
+ config: Config
1018
1154
  }) {
1019
1155
  try {
1020
1156
  const types = Object.entries(controls || ({} as PropertyControls))
@@ -1074,46 +1210,86 @@ export function propControlsToType({
1074
1210
  if (!name) {
1075
1211
  return ''
1076
1212
  }
1077
- return ` ${JSON.stringify(name)}?: ${typescriptType(value)}`
1213
+ return ` * ${name}?: ${typescriptType(value)} // ${value.title || name}`
1078
1214
  })
1079
1215
  .filter(Boolean)
1080
1216
  .join('\n')
1081
1217
 
1082
1218
  const componentName = componentCamelCase(fileName)
1083
1219
 
1084
- const defaultPropsTypes =
1085
- [
1086
- 'children?: React.ReactNode',
1087
- 'locale?: Locale',
1088
- 'style?: React.CSSProperties',
1089
- 'className?: string',
1090
- 'id?: string',
1091
- 'width?: any',
1092
- 'height?: any',
1093
- 'layoutId?: string',
1094
- ]
1095
- .map((line) => ` ${line}`)
1096
- .join('\n') + '\n'
1097
- let t = ''
1098
- t += '/* This file was generated by Unframer, do not edit manually */\n'
1099
-
1100
- t += 'import * as React from "react"\n\n'
1101
- t += 'import { UnframerBreakpoint } from "unframer"\n\n'
1102
- t += `type Locale = ${
1103
- config.locales?.length
1104
- ? config.locales.map((l) => `'${l.code}'`).join(' | ')
1105
- : 'string'
1106
- }\n`
1107
- t += `export interface Props {\n${defaultPropsTypes}${types}\n}\n\n`
1108
- t += `const ${componentName} = (props: Props) => any\n\n`
1109
- t += `type VariantsMap = Partial<Record<UnframerBreakpoint, Props['variant']>> & { base: Props['variant'] }\n\n`
1110
- t += `${componentName}.Responsive = (props: Omit<Props, 'variant'> & {variants?: VariantsMap}) => any\n\n`
1111
- t += `export default ${componentName}\n\n`
1112
-
1113
- return t
1220
+ const defaultPropsJsDoc = [
1221
+ ' * children?: React.ReactNode',
1222
+ ' * locale?: Locale',
1223
+ ' * style?: React.CSSProperties',
1224
+ ' * className?: string',
1225
+ ' * id?: string',
1226
+ ' * ref?: any',
1227
+ ' * width?: any',
1228
+ ' * height?: any',
1229
+ ' * layoutId?: string',
1230
+ ].join('\n')
1231
+
1232
+ // Generate header comment with type definitions
1233
+ let headerComment = '/**\n'
1234
+ headerComment += ' * @typedef Locale\n'
1235
+
1236
+ // Generate union type from config.locales if available
1237
+ const localeType = (() => {
1238
+ if (
1239
+ config?.locales &&
1240
+ Array.isArray(config.locales) &&
1241
+ config.locales.length > 0
1242
+ ) {
1243
+ return config.locales
1244
+ .map((locale) => `'${locale.slug}'`)
1245
+ .join(' | ')
1246
+ }
1247
+ return 'string'
1248
+ })()
1249
+
1250
+ headerComment += ` * ${localeType}\n`
1251
+ headerComment += ' */\n\n'
1252
+ headerComment += '/**\n'
1253
+ headerComment +=
1254
+ ' * @typedef {{\n'
1255
+ headerComment += defaultPropsJsDoc
1256
+
1257
+ if (types) {
1258
+ headerComment += '\n' + types
1259
+ }
1260
+ headerComment += `\n}} Props\n`
1261
+ headerComment += '\n */\n\n'
1262
+ headerComment += '/**\n'
1263
+ headerComment += ' * @type {import("unframer").UnframerBreakpoint}\n'
1264
+ headerComment += ' * Represents a responsive breakpoint for unframer.\n'
1265
+ headerComment += ' */\n\n'
1266
+ headerComment += '/**\n'
1267
+ headerComment += ' * @typedef VariantsMap\n'
1268
+ headerComment +=
1269
+ " * Partial record of UnframerBreakpoint to Props.variant, with a mandatory 'base' key.\n"
1270
+ headerComment +=
1271
+ " * { [key in UnframerBreakpoint]?: Props['variant'] } & { base: Props['variant'] }\n"
1272
+ headerComment += ' */'
1273
+
1274
+ // Generate responsive comment
1275
+ const responsiveComment = `/**\n * Renders ${componentName} for all breakpoints with a variants map. Variant prop is inferred per breakpoint.\n * @function\n * @param {Omit<Props, 'variant'> & {variants?: VariantsMap}} props\n * @returns {any}\n */`
1276
+
1277
+ // Generate default export comment - use inline function type instead of referencing undefined type
1278
+ const defaultExportComment = `/** @type {function(Props): any} */`
1279
+
1280
+ return {
1281
+ headerComment,
1282
+ responsiveComment,
1283
+ defaultExportComment,
1284
+ }
1114
1285
  } catch (e: any) {
1115
- logger.error('cannot generate types', e.stack)
1116
- return ''
1286
+ logger.error(e.message)
1287
+ logger.error('cannot generate typedoc comments', e.stack)
1288
+ return {
1289
+ headerComment: '',
1290
+ responsiveComment: '',
1291
+ defaultExportComment: '',
1292
+ }
1117
1293
  }
1118
1294
  }
1119
1295
 
@@ -1322,7 +1498,7 @@ async function recursiveReaddir(dir: string): Promise<string[]> {
1322
1498
  return files.flat()
1323
1499
  }
1324
1500
 
1325
- function indentWithTabs(str: string, tabs: string) {
1501
+ export function indentWithTabs(str: string, tabs: string) {
1326
1502
  if (!str) return ''
1327
1503
  return str
1328
1504
  .split('\n')
@@ -1337,8 +1513,10 @@ export async function createExampleComponentCode({
1337
1513
  outDir: string
1338
1514
  config: Config
1339
1515
  }) {
1340
- const outDirForExample = path.posix
1516
+ const outDirForExample = path
1341
1517
  .relative(process.cwd(), outDir)
1518
+ .split(path.sep)
1519
+ .join('/')
1342
1520
  .replace(/^src\//, '') // remove src so file works inside src
1343
1521
  const instances = config?.componentInstancesInIndexPage?.sort((a, b) => {
1344
1522
  // Order first by nodeDepth (lower is better)
@@ -1391,6 +1569,7 @@ export async function createExampleComponentCode({
1391
1569
  };
1392
1570
  `
1393
1571
  return {
1572
+ outDirForExample,
1394
1573
  exampleCode,
1395
1574
  }
1396
1575
  }
@@ -1404,3 +1583,35 @@ type BundleResult = {
1404
1583
  propertyControls?: PropertyControls
1405
1584
  }>
1406
1585
  }
1586
+
1587
+ /**
1588
+ * Compares two semantic version strings.
1589
+ * Returns true if versionB is greater than versionA.
1590
+ * Handles x.y.z, x.y, x, and optional pre-release (-alpha, etc).
1591
+ */
1592
+ export function isVersionGreater(versionA: string, versionB: string): boolean {
1593
+ try {
1594
+ function parseVersion(version: string) {
1595
+ // Remove pre-release (e.g. -alpha.1)
1596
+ let [core] = version.trim().split('-')
1597
+ return core.split('.').map((x) => parseInt(x, 10))
1598
+ }
1599
+ const [a1 = 0, a2 = 0, a3 = 0] = parseVersion(versionA)
1600
+ const [b1 = 0, b2 = 0, b3 = 0] = parseVersion(versionB)
1601
+
1602
+ if (b1 > a1) return true
1603
+ if (b1 < a1) return false
1604
+ if (b2 > a2) return true
1605
+ if (b2 < a2) return false
1606
+ if (b3 > a3) return true
1607
+ if (b3 < a3) return false
1608
+
1609
+ // If all equal, not greater
1610
+ return false
1611
+ } catch (error) {
1612
+ spinner.error(
1613
+ `Error comparing versions "${versionA}" and "${versionB}": ${error?.stack || error?.message || error}`,
1614
+ )
1615
+ return true
1616
+ }
1617
+ }