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/esm/exporter.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { build, context } from 'esbuild';
2
- import packageJson from '../package.json';
3
2
  import url from 'url';
4
3
  import { Sema } from 'async-sema';
5
4
  import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill';
@@ -10,12 +9,15 @@ import fs from 'fs';
10
9
  import path from 'path';
11
10
  import { dedent } from './utils.js';
12
11
  import { babelPluginJsxTransform, removeJsxExpressionContainer, } from './babel-jsx.js';
12
+ import { babelPluginTypedoc } from './babel-typedoc.js';
13
13
  import { propCamelCaseJustLikeFramer } from './compat.js';
14
14
  import { breakpointsStyles, defaultBreakpointSizes, getFontsStyles, groupBy, logFontsUsage, } from './css.js';
15
15
  import { defaultExternalPackages, esbuildPluginBundleDependencies, resolveRedirect, } from './esbuild';
16
16
  import { ControlType, combinedCSSRules, } from './framer.js';
17
17
  import { notifyError } from './sentry';
18
18
  import { kebabCase, logger, spinner, stackblitzDemoExample, terminalMarkdown, } from './utils.js';
19
+ import { installPackagesBatch } from './package-manager.js';
20
+ import { version as currentUnframerVersion } from './version.js';
19
21
  import { Biome, Distribution } from '@biomejs/js-api';
20
22
  let biome;
21
23
  export async function bundle({ config, cwd: out = '', watch = false, signal = undefined, }) {
@@ -26,6 +28,24 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
26
28
  await fs.promises.mkdir(out, { recursive: true });
27
29
  }
28
30
  catch (e) { }
31
+ // Prefix for temporary .js files to avoid HMR issues
32
+ const tempJsPrefix = 'temp_';
33
+ // Helper function to handle file path transformations with temp prefix
34
+ function getFilePaths(filePath, outDir) {
35
+ const baseName = path.basename(filePath);
36
+ const dirName = path.dirname(filePath);
37
+ const tempFileName = tempJsPrefix + baseName;
38
+ const tempFilePath = path.join(dirName, tempFileName);
39
+ return {
40
+ originalPath: filePath,
41
+ tempJsPath: path.resolve(outDir, tempFilePath),
42
+ finalJsPath: path.resolve(outDir, filePath),
43
+ jsxPath: path.resolve(outDir, filePath.replace(/\.js$/, '.jsx')),
44
+ tempFilePath,
45
+ baseName,
46
+ dirName,
47
+ };
48
+ }
29
49
  spinner.start('exporting components...');
30
50
  const otherRoutes = Object.fromEntries((config.framerWebPages || []).map((page) => [
31
51
  page.webPageId,
@@ -42,7 +62,7 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
42
62
  };
43
63
  }
44
64
  const fn = watch ? context : fakeContext;
45
- let foundError = false;
65
+ const missingPackages = new Set();
46
66
  const buildContext = await fn({
47
67
  absWorkingDir: out,
48
68
  entryPoints: Object.keys(components)
@@ -71,9 +91,12 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
71
91
  signal,
72
92
  externalPackages: config.externalPackages,
73
93
  externalizeNpm: config.allExternal,
74
- outDir: config.outDir,
94
+ outDir: out,
75
95
  onMissingPackage: (e) => {
76
- foundError = true;
96
+ // No longer needed - packages are auto-installed
97
+ },
98
+ onCollectMissingPackage: (pkg) => {
99
+ missingPackages.add(pkg);
77
100
  },
78
101
  }),
79
102
  nodeModulesPolyfillPlugin({}),
@@ -88,6 +111,13 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
88
111
  });
89
112
  build.onLoad({ filter: /.*/, namespace: 'virtual' }, async (args) => {
90
113
  const name = args.path;
114
+ // Handle virtual routes module
115
+ if (name === '__routes') {
116
+ return {
117
+ contents: `export const routes = ${JSON.stringify(otherRoutes, null, 2)};`,
118
+ loader: 'js',
119
+ };
120
+ }
91
121
  const url = components[name];
92
122
  const componentBreakpoints = config.componentBreakpoints?.filter((x) => x.componentName === name) || [];
93
123
  const brk = breakpointSizes
@@ -109,6 +139,8 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
109
139
  ...brk.slice(1),
110
140
  ])
111
141
  : {};
142
+ // Use virtual routes module
143
+ const routesImportPath = 'virtual:__routes';
112
144
  return {
113
145
  contents: /** js **/ `
114
146
  'use client'
@@ -119,30 +151,31 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
119
151
  signal,
120
152
  })}'
121
153
  import { WithFramerBreakpoints } from 'unframer'
154
+ import { routes } from '${routesImportPath}'
122
155
  const locales = ${JSON.stringify(config.locales) || '[]'}
123
156
  const defaultResponsiveVariants = ${JSON.stringify(responsiveVariants, null, 2)}
124
157
 
125
- Component.Responsive = ({ locale, ...rest }) => {
158
+
159
+ function ComponentWithRoot({ locale, ...rest }) {
126
160
  return (
127
161
  <ContextProviders
128
- routes={${JSON.stringify(otherRoutes)}}
129
- children={<WithFramerBreakpoints
130
- Component={Component}
131
- variants={defaultResponsiveVariants}
132
- {...rest}
133
- />}
162
+ routes={routes}
163
+ children={<Component {...rest} />}
134
164
  framerSiteId={${JSON.stringify(config.fullFramerProjectId)}}
135
165
  locale={locale}
136
166
  locales={locales}
137
167
  />
138
168
  )
139
169
  }
140
-
141
- export default function ComponentWithRoot({ locale, ...rest }) {
170
+ ComponentWithRoot.Responsive = ({ locale, ...rest }) => {
142
171
  return (
143
172
  <ContextProviders
144
- routes={${JSON.stringify(otherRoutes, null, 2)}}
145
- children={<Component {...rest} />}
173
+ routes={routes}
174
+ children={<WithFramerBreakpoints
175
+ Component={Component}
176
+ variants={defaultResponsiveVariants}
177
+ {...rest}
178
+ />}
146
179
  framerSiteId={${JSON.stringify(config.fullFramerProjectId)}}
147
180
  locale={locale}
148
181
  locales={locales}
@@ -150,6 +183,7 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
150
183
  )
151
184
  }
152
185
  Object.assign(ComponentWithRoot, Component)
186
+ export default ComponentWithRoot
153
187
  `,
154
188
  loader: 'jsx',
155
189
  };
@@ -162,10 +196,27 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
162
196
  });
163
197
  const doNotEditComment = `/* This file was generated by Unframer for Framer project ${config.projectId || ''} "${config.projectName}", do not edit manually */\n`;
164
198
  async function rebuild() {
199
+ // Clear missing packages for each rebuild (important for watch mode)
200
+ missingPackages.clear();
201
+ try {
202
+ const installedVersion = await resolvePackageVersion({
203
+ cwd: out,
204
+ pkg: 'unframer',
205
+ });
206
+ if (isVersionGreater(installedVersion || '0.0.0', currentUnframerVersion || '0.0.0')) {
207
+ // Version mismatch, add with specific version
208
+ missingPackages.add(`unframer@${currentUnframerVersion}`);
209
+ spinner.info(`Different unframer version detected (${installedVersion}), will install unframer@${currentUnframerVersion}`);
210
+ }
211
+ }
212
+ catch (e) {
213
+ // Unframer not installed, add with specific version
214
+ missingPackages.add(`unframer@${currentUnframerVersion}`);
215
+ spinner.info(`Missing package detected: unframer@${currentUnframerVersion}`);
216
+ }
165
217
  const prevFiles = await recursiveReaddir(out);
166
218
  const buildResult = await buildContext.rebuild().catch((e) => {
167
219
  if (e.message.includes('No matching export ')) {
168
- foundError = true;
169
220
  spinner.error(`esbuild failed to import from an external package, this usually means that the npm package version in Framer is older than the latest.`);
170
221
  spinner.error(`Use --external to make all npm packagess external, then install the right version`);
171
222
  process.exit(1);
@@ -173,105 +224,31 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
173
224
  throw e;
174
225
  });
175
226
  spinner.update('Finished build');
176
- for (let file of buildResult.outputFiles) {
177
- const resultPathAbsJs = path.resolve(out, file.path);
178
- const resultPathAbsJsx = resultPathAbsJs.replace(/\.js$/, '.jsx');
179
- const existing = await fs.promises
180
- .readFile(resultPathAbsJsx, 'utf-8')
181
- .catch(() => null);
182
- const tooBigSize = 0.7 * 1024 * 1024;
183
- let formatted = file.text;
184
- let tooBig = file.text.length >= tooBigSize;
185
- let didFormat = false;
186
- if (config.jsx &&
187
- !tooBig &&
188
- !resultPathAbsJs.includes('/chunks/') &&
189
- !resultPathAbsJs.includes('\\chunks\\')) {
190
- try {
191
- let res = transform(file.text || '', {
192
- babelrc: false,
193
- sourceType: 'module',
194
- plugins: [
195
- // babelPluginDeduplicateImports,
196
- babelPluginJsxTransform,
197
- removeJsxExpressionContainer,
198
- ],
199
- // ast: true,
200
- // code: false,
201
- filename: 'x.jsx',
202
- compact: false,
203
- sourceMaps: false,
204
- });
205
- if (res?.code) {
206
- if (!biome) {
207
- biome = await Biome.create({
208
- distribution: Distribution.NODE,
209
- });
210
- }
211
- let result = biome.formatContent(res.code, {
212
- filePath: 'example.jsx',
213
- });
214
- didFormat = true;
215
- formatted = result.content;
216
- }
217
- }
218
- catch (e) {
219
- notifyError(e, 'babel transform and format');
220
- }
227
+ // Install missing packages if any were collected
228
+ if (missingPackages.size > 0) {
229
+ const packagesToInstall = Array.from(missingPackages);
230
+ logger.log(`Installing missing packages: ${packagesToInstall.join(', ')}`);
231
+ const installResult = await installPackagesBatch({
232
+ packageNames: packagesToInstall,
233
+ cwd: out,
234
+ isDev: false,
235
+ });
236
+ if (!installResult.success) {
237
+ spinner.error(`Failed to install packages: ${installResult.error}`);
238
+ // Don't fail the build, just warn
221
239
  }
222
- // let inputCode = res!.code!
223
- // let shouldFormat = !tooBig && !file.path.includes('chunks')
224
- // if (shouldFormat) {
225
- // spinner.update(`Formatting ${path.relative(out, file.path)}`)
226
- // formatted = dprint.format('file.jsx', file.text, {
227
- // lineWidth: 140,
228
- // quoteStyle: 'alwaysSingle',
229
- // trailingCommas: 'always',
230
- // semiColons: 'always',
231
- // })
232
- // }
233
- // if (tooBig) {
234
- // spinner.info(
235
- // `skipping formatting ${path.relative(
236
- // out,
237
- // file.path,
238
- // )}, too big`,
239
- // )
240
- // }
241
- // if (framerWebPages?.length) {
242
- // codeNew = replaceWebPageIds({
243
- // code: codeNew,
244
- // elements: framerWebPages,
245
- // })
246
- // }
247
- // const lines = findRelativeLinks(codeNew)
248
- // if (lines.length) {
249
- // spinner.error(
250
- // `found broken links for ${path.relative(out, file.path)}`,
251
- // )
252
- // lines.forEach((line) => {
253
- // logger.log(`${path.resolve(out, file.path)}:${line + 1}`)
254
- // })
255
- // }
240
+ }
241
+ // First, write raw JS files for type extraction with temp prefix
242
+ for (let file of buildResult.outputFiles) {
243
+ const paths = getFilePaths(file.path, out);
256
244
  const prefix = `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment;
257
- const codeJsx = prefix + formatted;
258
245
  const codeJs = prefix + file.text;
259
- // if (existing === codeJsx) {
260
- // continue
261
- // }
262
- logger.log(`writing`, path.relative(out, file.path));
263
- await fs.promises.mkdir(path.dirname(resultPathAbsJsx), {
246
+ logger.log(`writing temp JS`, path.relative(out, paths.tempFilePath));
247
+ await fs.promises.mkdir(path.dirname(paths.tempJsPath), {
264
248
  recursive: true,
265
249
  });
266
- if (codeJs !== codeJsx || !didFormat) {
267
- await fs.promises.writeFile(resultPathAbsJs, codeJs, 'utf-8');
268
- }
269
- if (didFormat) {
270
- await fs.promises.writeFile(resultPathAbsJsx, codeJsx, 'utf-8');
271
- }
250
+ await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8');
272
251
  }
273
- spinner.stop();
274
- await fs.promises.writeFile(path.resolve(out, '.cursorignore'), `**/*.js\nchunks\n`, 'utf-8');
275
252
  if (!buildResult?.outputFiles) {
276
253
  throw new Error('Failed to generate result');
277
254
  }
@@ -287,7 +264,8 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
287
264
  const name = path
288
265
  .relative(out, file.path)
289
266
  .replace(/\.jsx?$/, '');
290
- const resultPathAbs = path.resolve(out, file.path);
267
+ const paths = getFilePaths(file.path, out);
268
+ const resultPathAbs = paths.tempJsPath;
291
269
  if (!components[name]) {
292
270
  return;
293
271
  }
@@ -296,6 +274,7 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
296
274
  return;
297
275
  }
298
276
  logger.log(`extracting types for ${name}`);
277
+ spinner.info(`Extracting types for component: ${name}`);
299
278
  spinner.update(`Extracting types for ${name}`);
300
279
  const { propertyControls, fonts } = await extractPropControlsUnsafe(resultPathAbs, name);
301
280
  if (!propertyControls) {
@@ -305,17 +284,20 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
305
284
  ...x,
306
285
  fileName: path.basename(file.path),
307
286
  })));
308
- const types = propControlsToType({
287
+ const typedocComments = propControlsToTypedocComments({
309
288
  controls: propertyControls,
310
289
  fileName: name,
311
290
  config,
312
291
  });
292
+ logger.log(`Generated TypeDoc comments for ${name}: ${!!typedocComments.headerComment}`);
313
293
  await fs.promises.mkdir(out, { recursive: true });
314
- await fs.promises.writeFile(path.resolve(out, `${name}.d.ts`), types);
294
+ // .d.ts generation removed – types are now injected as typedoc
295
+ // comments directly inside the generated JSX file.
315
296
  return {
316
297
  propertyControls,
317
298
  fonts,
318
299
  name,
300
+ typedocComments,
319
301
  };
320
302
  }
321
303
  finally {
@@ -329,7 +311,6 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
329
311
  // Ignore error if file doesn't exist or can't be deleted
330
312
  }
331
313
  });
332
- // spinner.stop()
333
314
  const cssString = doNotEditComment +
334
315
  '/* This css file has all the necessary styles to run all your components */\n' +
335
316
  '\n' +
@@ -347,24 +328,27 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
347
328
  .forEach((x) => logger.log(x));
348
329
  const jsxFiles = buildResult.outputFiles
349
330
  .filter((x) => x.path.endsWith('.js') &&
350
- fs.existsSync(x.path.replace(/\.js$/, '.jsx')))
351
- .map((x) => x.path.replace(/\.js$/, '.jsx'));
331
+ fs.existsSync(getFilePaths(x.path, out).jsxPath))
332
+ .map((x) => getFilePaths(x.path, out).jsxPath);
352
333
  const outFiles = buildResult.outputFiles
353
- .map((x) => path.resolve(out, x.path))
334
+ .map((x) => {
335
+ const paths = getFilePaths(x.path, out);
336
+ if (x.path.endsWith('.js') && fs.existsSync(paths.jsxPath)) {
337
+ return null; // Will be handled by jsx files
338
+ }
339
+ return paths.finalJsPath;
340
+ })
341
+ .filter(Boolean)
354
342
  .concat([
355
343
  path.resolve(out, 'meta.json'),
356
344
  path.resolve(out, 'tokens.css'),
357
345
  path.resolve(out, '.cursorignore'),
358
346
  path.resolve(out, 'styles.css'),
359
347
  ])
360
- .concat(jsxFiles)
361
- .concat(buildResult.outputFiles.map((x) => path.resolve(out, x.path.replace(/\.jsx?$/, '.d.ts'))));
348
+ .concat(jsxFiles);
362
349
  const filesToDelete = prevFiles
363
350
  .filter((x) => !outFiles.includes(x))
364
- .concat(buildResult.outputFiles
365
- .map((x) => x.path)
366
- .filter((js) => js.endsWith('.js') &&
367
- jsxFiles.some((x) => x.startsWith(js))));
351
+ .filter((x) => !x.includes(tempJsPrefix)); // Don't delete temp files here, they're handled separately
368
352
  for (let file of filesToDelete) {
369
353
  logger.log('deleting', path.relative(out, file));
370
354
  try {
@@ -381,12 +365,6 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
381
365
  if (watch) {
382
366
  logger.log('waiting for components or config changes');
383
367
  }
384
- if (!tokens?.length) {
385
- const tokensCss = "/* 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" +
386
- '/* Bug: https://www.framer.community/c/bugs/color-style-unlinks-when-copying-component-between-projects-resulting-in-potential-value-discrepancy */\n' +
387
- getTokensCss({ out, result: buildResult });
388
- await fs.promises.writeFile(path.resolve(out, 'tokens.css'), tokensCss, 'utf-8');
389
- }
390
368
  const res = {
391
369
  components: Object.entries(components).map(([name, v]) => {
392
370
  const propControls = propControlsData.find((x) => x?.name === name);
@@ -399,6 +377,131 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
399
377
  };
400
378
  }),
401
379
  };
380
+ // Process and write JSX files with TypeDoc comments
381
+ spinner.update('Processing JSX files with TypeDoc comments');
382
+ for (let file of buildResult.outputFiles) {
383
+ const paths = getFilePaths(file.path, out);
384
+ const componentName = path
385
+ .relative(out, file.path)
386
+ .replace(/\.js$/, '');
387
+ const propData = propControlsData.find((p) => p?.name === componentName);
388
+ const typedocComments = propData?.typedocComments;
389
+ logger.log(`Processing component: ${componentName}`);
390
+ spinner.update(`Processing JSX for ${componentName}`);
391
+ if (!propData) {
392
+ logger.log(` No propData found for ${componentName}`);
393
+ }
394
+ else {
395
+ logger.log(` PropData found for ${componentName}, has propertyControls: ${!!propData.propertyControls}`);
396
+ if (!typedocComments) {
397
+ logger.log(` No typedocComments for ${componentName}`);
398
+ }
399
+ else {
400
+ logger.log(` TypeDoc comments available for ${componentName}`);
401
+ }
402
+ }
403
+ const existing = await fs.promises
404
+ .readFile(paths.jsxPath, 'utf-8')
405
+ .catch(() => null);
406
+ const tooBigSize = 0.7 * 1024 * 1024;
407
+ let formatted = file.text;
408
+ let tooBig = file.text.length >= tooBigSize;
409
+ let didFormat = false;
410
+ if (config.jsx &&
411
+ !tooBig &&
412
+ !paths.tempJsPath.includes('/chunks/') &&
413
+ !paths.tempJsPath.includes('\\chunks\\')) {
414
+ try {
415
+ const plugins = [
416
+ // babelPluginDeduplicateImports,
417
+ babelPluginJsxTransform,
418
+ removeJsxExpressionContainer,
419
+ ];
420
+ // Add TypeDoc plugin if we have comments for this component
421
+ if (typedocComments) {
422
+ logger.log(` Adding TypeDoc plugin for ${componentName}`);
423
+ plugins.push(babelPluginTypedoc(typedocComments));
424
+ }
425
+ else {
426
+ logger.log(` No TypeDoc comments to add for ${componentName}`);
427
+ }
428
+ let res = transform(file.text || '', {
429
+ babelrc: false,
430
+ sourceType: 'module',
431
+ parserOpts: {
432
+ plugins: ['jsx'],
433
+ },
434
+ plugins,
435
+ // ast: true,
436
+ // code: false,
437
+ filename: 'x.jsx',
438
+ compact: false,
439
+ sourceMaps: false,
440
+ });
441
+ if (res?.code) {
442
+ if (!biome) {
443
+ biome = await Biome.create({
444
+ distribution: Distribution.NODE,
445
+ });
446
+ }
447
+ let result = biome.formatContent(res.code, {
448
+ filePath: 'example.jsx',
449
+ });
450
+ didFormat = true;
451
+ formatted = result.content;
452
+ }
453
+ }
454
+ catch (e) {
455
+ notifyError(e, 'babel transform and format');
456
+ }
457
+ }
458
+ const prefix = `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment;
459
+ const codeJsx = prefix + formatted;
460
+ const codeJs = prefix + file.text;
461
+ logger.log(`writing`, path.relative(out, file.path));
462
+ await fs.promises.mkdir(path.dirname(paths.jsxPath), {
463
+ recursive: true,
464
+ });
465
+ // Always write the temp .js file for type extraction
466
+ await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8');
467
+ // Only write .jsx file if it's different from existing or if formatting was done
468
+ if (didFormat && codeJsx !== existing) {
469
+ await fs.promises.writeFile(paths.jsxPath, codeJsx, 'utf-8');
470
+ }
471
+ }
472
+ spinner.stop();
473
+ // await fs.promises.writeFile(
474
+ // path.resolve(out, '.cursorignore'),
475
+ // `**/*.js\nchunks\n`,
476
+ // 'utf-8',
477
+ // )
478
+ // Clean up temp .js files and handle prefixes
479
+ for (let file of buildResult.outputFiles) {
480
+ if (file.path.endsWith('.js')) {
481
+ const paths = getFilePaths(file.path, out);
482
+ if (fs.existsSync(paths.jsxPath)) {
483
+ // Remove temp .js file if .jsx equivalent exists
484
+ logger.log('removing temp JS file with JSX equivalent:', path.relative(out, paths.tempJsPath));
485
+ try {
486
+ await fs.promises.rm(paths.tempJsPath);
487
+ await fs.promises.rm(paths.finalJsPath);
488
+ }
489
+ catch (error) {
490
+ // Ignore error if file doesn't exist
491
+ }
492
+ }
493
+ else {
494
+ // Rename temp .js file to final name if no .jsx equivalent
495
+ logger.log('renaming temp JS file to final name:', path.relative(out, paths.tempJsPath), '->', path.relative(out, paths.finalJsPath));
496
+ try {
497
+ await fs.promises.rename(paths.tempJsPath, paths.finalJsPath);
498
+ }
499
+ catch (error) {
500
+ // Ignore error if file doesn't exist
501
+ }
502
+ }
503
+ }
504
+ }
402
505
  spinner.info(`Build completed`);
403
506
  return res;
404
507
  }
@@ -424,8 +527,11 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
424
527
  const result = await rebuild();
425
528
  console.log();
426
529
  console.log();
427
- const outDirForExample = path.posix.relative(process.cwd(), out).replace(/^src\//, '') ||
428
- 'framer'; // remove src so file works inside src
530
+ const outDirForExample = path
531
+ .relative(process.cwd(), out)
532
+ .split(path.sep)
533
+ .join('/')
534
+ .replace(/^src\//, '') || 'framer'; // remove src so file works inside src
429
535
  const { exampleCode } = await createExampleComponentCode({
430
536
  outDir: out,
431
537
  // buildResult: result,
@@ -438,38 +544,31 @@ export async function bundle({ config, cwd: out = '', watch = false, signal = un
438
544
  });
439
545
  await fs.promises.writeFile(stackblitzDemoExample, exampleCode);
440
546
  }
441
- if (!foundError) {
442
- console.log(terminalMarkdown(dedent `
443
- # How to use the Framer components
547
+ console.log(terminalMarkdown(dedent `
548
+ # How to use the Framer components
444
549
 
445
- Your components are exported to \`${outDirForExample}\` folder. Now please install the \`unframer\` runtime dependency:
550
+ Your components are exported to \`${outDirForExample}\` folder.
446
551
 
447
- \`\`\`sh
448
- npm install unframer
449
- \`\`\`
552
+ Each component has a \`.Responsive\` variant that allows you to specify different variants for different breakpoints.
450
553
 
451
- Each component has a \`.Responsive\` variant that allows you to specify different variants for different breakpoints.
554
+ You can use the components like this (try copy pasting the code below into your React app):
452
555
 
453
- You can use the components like this (try copy pasting the code below into your React app):
556
+ \`\`\`jsx
557
+ ${exampleCode}
558
+ \`\`\`
454
559
 
455
- \`\`\`jsx
456
- ${exampleCode}
457
- \`\`\`
560
+ It's very important to import the \`styles.css\` file to include the necessary styles for the components.
458
561
 
459
- It's very important to import the \`styles.css\` file to include the necessary styles for the components.
562
+ To style components you can pass a \`style\` or \`className\` prop (but remember to use !important to increase the specificity).
460
563
 
461
- To style components you can pass a \`style\` or \`className\` prop (but remember to use !important to increase the specificity).
564
+ Read more on GitHub: https://github.com/remorses/unframer
462
565
 
463
- Read more on GitHub: https://github.com/remorses/unframer
464
-
465
- `));
466
- }
467
- await checkUnframerVersion({ cwd: out });
566
+ `));
468
567
  console.log();
469
568
  return { result, rebuild, buildContext };
470
569
  }
471
570
  const packageVersionCache = new Map();
472
- export function resolvePackage({ cwd, pkg }) {
571
+ export function resolvePackageVersion({ cwd, pkg }) {
473
572
  if (packageVersionCache.has(pkg)) {
474
573
  return Promise.resolve(packageVersionCache.get(pkg));
475
574
  }
@@ -484,7 +583,7 @@ export function resolvePackage({ cwd, pkg }) {
484
583
  cwd,
485
584
  }, (error, stdout, stderr) => {
486
585
  if (error) {
487
- logger.log(stderr);
586
+ // Package not installed - this is expected and handled by auto-install
488
587
  reject(new Error(`${pkg} is not installed in your project`));
489
588
  return;
490
589
  }
@@ -494,17 +593,25 @@ export function resolvePackage({ cwd, pkg }) {
494
593
  });
495
594
  });
496
595
  }
497
- export async function checkUnframerVersion({ cwd }) {
498
- const currentVersion = packageJson.version;
499
- try {
500
- const installedVersion = await resolvePackage({ cwd, pkg: 'unframer' });
501
- if (installedVersion !== currentVersion) {
502
- spinner.error(`IMPORTANT: Unframer version mismatch. Please run: npm update unframer@latest`);
503
- }
504
- }
505
- catch (e) {
506
- spinner.error('IMPORTANT: Unframer is not installed in your project. Please run: npm install unframer');
507
- }
596
+ export function resolvePackage({ cwd, pkg }) {
597
+ return new Promise((resolve) => {
598
+ const code = `import('${pkg}/package.json', { with: { type: 'json' } }).then(()=>console.log('true')).catch(()=>import('${pkg}').then(()=>console.log('true')).catch(()=>console.log('false')));`;
599
+ const command = [
600
+ JSON.stringify(nodePath),
601
+ '-e',
602
+ JSON.stringify(code),
603
+ ].join(' ');
604
+ exec(command, {
605
+ cwd,
606
+ }, (error, stdout) => {
607
+ if (error) {
608
+ resolve(false);
609
+ return;
610
+ }
611
+ const exists = stdout.trim().split('\n').pop() === 'true';
612
+ resolve(exists);
613
+ });
614
+ });
508
615
  }
509
616
  export function getDarkModeSelector(opts) {
510
617
  const { darkModeType = 'class', content } = opts;
@@ -598,7 +705,7 @@ async function extractPropControlsSafe(text, name) {
598
705
  logger.error(`Cannot get property controls for ${name}`, e.stack);
599
706
  }
600
707
  }
601
- function getTokensCss({ out, result, }) {
708
+ async function getTokensCss({ out, result, }) {
602
709
  const allTokens = [];
603
710
  for (let file of result.outputFiles) {
604
711
  const code = fs.readFileSync(path.resolve(out, file.path), 'utf-8');
@@ -728,7 +835,11 @@ function safeJsonParse(text) {
728
835
  return null;
729
836
  }
730
837
  }
731
- export function propControlsToType({ config, fileName, controls, }) {
838
+ /**
839
+ * Generates TypeDoc comments that will be injected into JSX files
840
+ * instead of generating separate .d.ts files
841
+ */
842
+ export function propControlsToTypedocComments({ config, fileName, controls, }) {
732
843
  try {
733
844
  const types = Object.entries(controls || {})
734
845
  .map(([key, value]) => {
@@ -786,40 +897,76 @@ export function propControlsToType({ config, fileName, controls, }) {
786
897
  if (!name) {
787
898
  return '';
788
899
  }
789
- return ` ${JSON.stringify(name)}?: ${typescriptType(value)}`;
900
+ return ` * ${name}?: ${typescriptType(value)} // ${value.title || name}`;
790
901
  })
791
902
  .filter(Boolean)
792
903
  .join('\n');
793
904
  const componentName = componentCamelCase(fileName);
794
- const defaultPropsTypes = [
795
- 'children?: React.ReactNode',
796
- 'locale?: Locale',
797
- 'style?: React.CSSProperties',
798
- 'className?: string',
799
- 'id?: string',
800
- 'width?: any',
801
- 'height?: any',
802
- 'layoutId?: string',
803
- ]
804
- .map((line) => ` ${line}`)
805
- .join('\n') + '\n';
806
- let t = '';
807
- t += '/* This file was generated by Unframer, do not edit manually */\n';
808
- t += 'import * as React from "react"\n\n';
809
- t += 'import { UnframerBreakpoint } from "unframer"\n\n';
810
- t += `type Locale = ${config.locales?.length
811
- ? config.locales.map((l) => `'${l.code}'`).join(' | ')
812
- : 'string'}\n`;
813
- t += `export interface Props {\n${defaultPropsTypes}${types}\n}\n\n`;
814
- t += `const ${componentName} = (props: Props) => any\n\n`;
815
- t += `type VariantsMap = Partial<Record<UnframerBreakpoint, Props['variant']>> & { base: Props['variant'] }\n\n`;
816
- t += `${componentName}.Responsive = (props: Omit<Props, 'variant'> & {variants?: VariantsMap}) => any\n\n`;
817
- t += `export default ${componentName}\n\n`;
818
- return t;
905
+ const defaultPropsJsDoc = [
906
+ ' * children?: React.ReactNode',
907
+ ' * locale?: Locale',
908
+ ' * style?: React.CSSProperties',
909
+ ' * className?: string',
910
+ ' * id?: string',
911
+ ' * ref?: any',
912
+ ' * width?: any',
913
+ ' * height?: any',
914
+ ' * layoutId?: string',
915
+ ].join('\n');
916
+ // Generate header comment with type definitions
917
+ let headerComment = '/**\n';
918
+ headerComment += ' * @typedef Locale\n';
919
+ // Generate union type from config.locales if available
920
+ const localeType = (() => {
921
+ if (config?.locales &&
922
+ Array.isArray(config.locales) &&
923
+ config.locales.length > 0) {
924
+ return config.locales
925
+ .map((locale) => `'${locale.slug}'`)
926
+ .join(' | ');
927
+ }
928
+ return 'string';
929
+ })();
930
+ headerComment += ` * ${localeType}\n`;
931
+ headerComment += ' */\n\n';
932
+ headerComment += '/**\n';
933
+ headerComment +=
934
+ ' * @typedef {{\n';
935
+ headerComment += defaultPropsJsDoc;
936
+ if (types) {
937
+ headerComment += '\n' + types;
938
+ }
939
+ headerComment += `\n}} Props\n`;
940
+ headerComment += '\n */\n\n';
941
+ headerComment += '/**\n';
942
+ headerComment += ' * @type {import("unframer").UnframerBreakpoint}\n';
943
+ headerComment += ' * Represents a responsive breakpoint for unframer.\n';
944
+ headerComment += ' */\n\n';
945
+ headerComment += '/**\n';
946
+ headerComment += ' * @typedef VariantsMap\n';
947
+ headerComment +=
948
+ " * Partial record of UnframerBreakpoint to Props.variant, with a mandatory 'base' key.\n";
949
+ headerComment +=
950
+ " * { [key in UnframerBreakpoint]?: Props['variant'] } & { base: Props['variant'] }\n";
951
+ headerComment += ' */';
952
+ // Generate responsive comment
953
+ 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 */`;
954
+ // Generate default export comment - use inline function type instead of referencing undefined type
955
+ const defaultExportComment = `/** @type {function(Props): any} */`;
956
+ return {
957
+ headerComment,
958
+ responsiveComment,
959
+ defaultExportComment,
960
+ };
819
961
  }
820
962
  catch (e) {
821
- logger.error('cannot generate types', e.stack);
822
- return '';
963
+ logger.error(e.message);
964
+ logger.error('cannot generate typedoc comments', e.stack);
965
+ return {
966
+ headerComment: '',
967
+ responsiveComment: '',
968
+ defaultExportComment: '',
969
+ };
823
970
  }
824
971
  }
825
972
  export function parsePropertyControls(code) {
@@ -995,7 +1142,7 @@ async function recursiveReaddir(dir) {
995
1142
  }));
996
1143
  return files.flat();
997
1144
  }
998
- function indentWithTabs(str, tabs) {
1145
+ export function indentWithTabs(str, tabs) {
999
1146
  if (!str)
1000
1147
  return '';
1001
1148
  return str
@@ -1004,8 +1151,10 @@ function indentWithTabs(str, tabs) {
1004
1151
  .join('\n');
1005
1152
  }
1006
1153
  export async function createExampleComponentCode({ outDir, config, }) {
1007
- const outDirForExample = path.posix
1154
+ const outDirForExample = path
1008
1155
  .relative(process.cwd(), outDir)
1156
+ .split(path.sep)
1157
+ .join('/')
1009
1158
  .replace(/^src\//, ''); // remove src so file works inside src
1010
1159
  const instances = config?.componentInstancesInIndexPage?.sort((a, b) => {
1011
1160
  // Order first by nodeDepth (lower is better)
@@ -1051,7 +1200,42 @@ export async function createExampleComponentCode({ outDir, config, }) {
1051
1200
  };
1052
1201
  `;
1053
1202
  return {
1203
+ outDirForExample,
1054
1204
  exampleCode,
1055
1205
  };
1056
1206
  }
1207
+ /**
1208
+ * Compares two semantic version strings.
1209
+ * Returns true if versionB is greater than versionA.
1210
+ * Handles x.y.z, x.y, x, and optional pre-release (-alpha, etc).
1211
+ */
1212
+ export function isVersionGreater(versionA, versionB) {
1213
+ try {
1214
+ function parseVersion(version) {
1215
+ // Remove pre-release (e.g. -alpha.1)
1216
+ let [core] = version.trim().split('-');
1217
+ return core.split('.').map((x) => parseInt(x, 10));
1218
+ }
1219
+ const [a1 = 0, a2 = 0, a3 = 0] = parseVersion(versionA);
1220
+ const [b1 = 0, b2 = 0, b3 = 0] = parseVersion(versionB);
1221
+ if (b1 > a1)
1222
+ return true;
1223
+ if (b1 < a1)
1224
+ return false;
1225
+ if (b2 > a2)
1226
+ return true;
1227
+ if (b2 < a2)
1228
+ return false;
1229
+ if (b3 > a3)
1230
+ return true;
1231
+ if (b3 < a3)
1232
+ return false;
1233
+ // If all equal, not greater
1234
+ return false;
1235
+ }
1236
+ catch (error) {
1237
+ spinner.error(`Error comparing versions "${versionA}" and "${versionB}": ${error?.stack || error?.message || error}`);
1238
+ return true;
1239
+ }
1240
+ }
1057
1241
  //# sourceMappingURL=exporter.js.map