@reframejs/unframer 4.2.0-reframe.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +389 -0
  2. package/bin.js +5 -0
  3. package/dist/babel-jsx.d.ts +24 -0
  4. package/dist/babel-jsx.js +299 -0
  5. package/dist/babel-plugin-imports.d.ts +15 -0
  6. package/dist/babel-plugin-imports.js +211 -0
  7. package/dist/babel-typedoc.d.ts +39 -0
  8. package/dist/babel-typedoc.js +76 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.js +3 -0
  11. package/dist/cli.d.ts +59 -0
  12. package/dist/cli.js +821 -0
  13. package/dist/compat.d.ts +2 -0
  14. package/dist/compat.js +23 -0
  15. package/dist/config.d.ts +18 -0
  16. package/dist/config.js +27 -0
  17. package/dist/css-core.d.ts +50 -0
  18. package/dist/css-core.js +231 -0
  19. package/dist/css.d.ts +15 -0
  20. package/dist/css.js +36 -0
  21. package/dist/esbuild.d.ts +31 -0
  22. package/dist/esbuild.js +317 -0
  23. package/dist/exporter.d.ts +96 -0
  24. package/dist/exporter.js +1151 -0
  25. package/dist/flat-cache-interceptor.d.ts +24 -0
  26. package/dist/flat-cache-interceptor.js +87 -0
  27. package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
  28. package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
  29. package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
  30. package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
  31. package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
  32. package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
  33. package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
  34. package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
  35. package/dist/framer-client.server.d.ts +4 -0
  36. package/dist/framer-client.server.js +45 -0
  37. package/dist/framer-package-versions.d.ts +781 -0
  38. package/dist/framer-package-versions.js +782 -0
  39. package/dist/framer-types.d.ts +30 -0
  40. package/dist/framer-types.js +35 -0
  41. package/dist/framer.d.ts +4990 -0
  42. package/dist/framer.js +64296 -0
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +13 -0
  45. package/dist/package-manager.d.ts +10 -0
  46. package/dist/package-manager.js +138 -0
  47. package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
  48. package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
  49. package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
  50. package/dist/plugin-mcp-dist/lib/cms.js +58 -0
  51. package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
  52. package/dist/plugin-mcp-dist/lib/errors.js +48 -0
  53. package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
  54. package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
  55. package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +3 -0
  56. package/dist/plugin-mcp-dist/lib/framer-client.server.js +46 -0
  57. package/dist/plugin-mcp-dist/lib/framer.d.ts +30 -0
  58. package/dist/plugin-mcp-dist/lib/framer.js +1084 -0
  59. package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
  60. package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
  61. package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +142 -0
  62. package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
  63. package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +385 -0
  64. package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1924 -0
  65. package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
  66. package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
  67. package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
  68. package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
  69. package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
  70. package/dist/plugin-mcp-dist/lib/plugin-websocket.js +170 -0
  71. package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
  72. package/dist/plugin-mcp-dist/lib/react-export.js +358 -0
  73. package/dist/plugin-mcp-dist/lib/schema.d.ts +276 -0
  74. package/dist/plugin-mcp-dist/lib/schema.js +907 -0
  75. package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
  76. package/dist/plugin-mcp-dist/lib/store.js +10 -0
  77. package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
  78. package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
  79. package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
  80. package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
  81. package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
  82. package/dist/plugin-mcp-dist/lib/types.js +188 -0
  83. package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
  84. package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
  85. package/dist/plugin-mcp-dist/lib/utils.d.ts +13 -0
  86. package/dist/plugin-mcp-dist/lib/utils.js +57 -0
  87. package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
  88. package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
  89. package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
  90. package/dist/plugin-mcp-dist/lib/xml.js +395 -0
  91. package/dist/react.d.ts +76 -0
  92. package/dist/react.js +280 -0
  93. package/dist/renamer.d.ts +11 -0
  94. package/dist/renamer.js +139 -0
  95. package/dist/sentry.d.ts +2 -0
  96. package/dist/sentry.js +5 -0
  97. package/dist/stackblitz.d.ts +9 -0
  98. package/dist/stackblitz.js +179 -0
  99. package/dist/styles/framer.css +920 -0
  100. package/dist/styles/reset.css +37 -0
  101. package/dist/typescript.d.ts +24 -0
  102. package/dist/typescript.js +197 -0
  103. package/dist/undici-dispatcher.d.ts +2 -0
  104. package/dist/undici-dispatcher.js +10 -0
  105. package/dist/unframer-loader.d.ts +2 -0
  106. package/dist/unframer-loader.js +12 -0
  107. package/dist/utils.d.ts +20 -0
  108. package/dist/utils.js +81 -0
  109. package/dist/version.d.ts +2 -0
  110. package/dist/version.js +2 -0
  111. package/package.json +131 -0
@@ -0,0 +1,1151 @@
1
+ import { build, context } from 'esbuild';
2
+ import url from 'url';
3
+ import { createRequire } from 'node:module';
4
+ const require = createRequire(import.meta.url);
5
+ import { Sema } from 'async-sema';
6
+ import { transform } from '@babel/core';
7
+ import { exec } from 'child_process';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import crypto from 'crypto';
11
+ import { dedent } from './utils.js';
12
+ import { babelPluginJsxTransform, babelPluginSuppressHydration, removeJsxExpressionContainer, } from './babel-jsx.js';
13
+ import { babelPluginTypedoc } from './babel-typedoc.js';
14
+ import { breakpointsStyles, defaultBreakpointSizes, getFontsStyles, logFontsUsage, } from './css-core.js';
15
+ import { propControlsToTypedocComments, componentImportedName, } from './typescript.js';
16
+ import { defaultExternalPackages, esbuildPluginBundleDependencies, resolveRedirect, } from './esbuild.js';
17
+ import { notifyError } from './sentry.js';
18
+ import { kebabCase, logger, spinner, stackblitzDemoExample, } from './utils.js';
19
+ import pico from 'picocolors';
20
+ import { installPackagesBatch } from './package-manager.js';
21
+ import { version as currentUnframerVersion } from './version.js';
22
+ import { Biome, Distribution } from '@biomejs/js-api';
23
+ let biome;
24
+ function createComponentImportedNameResolver(componentPaths) {
25
+ const leafNameCounts = componentPaths.reduce((acc, componentPath) => {
26
+ const leafName = componentPath.split('/').filter(Boolean).pop() || '';
27
+ if (!leafName) {
28
+ return acc;
29
+ }
30
+ acc.set(leafName, (acc.get(leafName) || 0) + 1);
31
+ return acc;
32
+ }, new Map());
33
+ const duplicateLeafNames = new Set([...leafNameCounts.entries()]
34
+ .filter(([, count]) => {
35
+ return count > 1;
36
+ })
37
+ .map(([leafName]) => {
38
+ return leafName;
39
+ }));
40
+ return (componentPath) => {
41
+ return componentImportedName({
42
+ componentPath,
43
+ duplicateLeafNames,
44
+ });
45
+ };
46
+ }
47
+ const componentTypeCache = new Map();
48
+ // Helper to calculate file hash
49
+ async function getFileHash(filePath) {
50
+ const content = await fs.promises.readFile(filePath, 'utf-8');
51
+ return crypto.createHash('sha256').update(content).digest('hex');
52
+ }
53
+ export async function bundle({ config, cwd: out = '', watch = false, signal = undefined, metafile = false, }) {
54
+ const { components, breakpoints, tokens, framerWebPages } = config;
55
+ const getImportedName = createComponentImportedNameResolver(Object.keys(components));
56
+ out ||= path.resolve(process.cwd(), 'example');
57
+ out = path.resolve(out);
58
+ try {
59
+ await fs.promises.mkdir(out, { recursive: true });
60
+ }
61
+ catch (e) { }
62
+ // Prefix for temporary .js files to avoid HMR issues
63
+ const tempJsPrefix = 'temp_';
64
+ // Helper function to handle file path transformations with temp prefix
65
+ function getFilePaths(filePath, outDir) {
66
+ const baseName = path.basename(filePath);
67
+ const dirName = path.dirname(filePath);
68
+ const relativeDirname = path.relative(outDir, dirName);
69
+ // chunks are imported, which means they should not have the temp_ prefix or imports will fail
70
+ if (relativeDirname === 'chunks') {
71
+ return {
72
+ tempJsPath: path.resolve(outDir, filePath),
73
+ finalJsPath: path.resolve(outDir, filePath),
74
+ jsxPath: path.resolve(outDir, filePath.replace(/\.js$/, '.jsx')),
75
+ };
76
+ }
77
+ const tempFileName = tempJsPrefix + baseName;
78
+ const tempFilePath = path.join(dirName, tempFileName);
79
+ return {
80
+ tempJsPath: path.resolve(outDir, tempFilePath),
81
+ finalJsPath: path.resolve(outDir, filePath),
82
+ jsxPath: path.resolve(outDir, filePath.replace(/\.js$/, '.jsx')),
83
+ };
84
+ }
85
+ spinner.start('exporting components...');
86
+ const otherRoutes = Object.fromEntries((config.framerWebPages || []).map((page) => [
87
+ page.webPageId,
88
+ { path: page.path },
89
+ ]));
90
+ const breakpointSizes = Object.entries(config.breakpoints || defaultBreakpointSizes).sort(([, a], [, b]) => a - b);
91
+ function fakeContext(args) {
92
+ return {
93
+ rebuild() {
94
+ return build(args);
95
+ },
96
+ cancel() { },
97
+ dispose() { },
98
+ };
99
+ }
100
+ const fn = watch ? context : fakeContext;
101
+ const missingPackages = new Set();
102
+ const buildContext = await fn({
103
+ absWorkingDir: out,
104
+ entryPoints: Object.keys(components)
105
+ .filter((x) => x)
106
+ .map((name) => {
107
+ return {
108
+ in: `virtual:${name}`,
109
+ out: name,
110
+ };
111
+ }),
112
+ jsx: 'automatic',
113
+ // jsxFactory: '_jsx',
114
+ bundle: true,
115
+ platform: 'browser',
116
+ metafile: true,
117
+ format: 'esm',
118
+ minify: false,
119
+ treeShaking: true,
120
+ splitting: true,
121
+ logLevel: 'error',
122
+ pure: ['addPropertyControls'],
123
+ external: defaultExternalPackages,
124
+ chunkNames: 'chunks/[name]-[hash]',
125
+ plugins: [
126
+ esbuildPluginBundleDependencies({
127
+ signal,
128
+ externalPackages: config.externalPackages,
129
+ externalizeNpm: config.allExternal,
130
+ outDir: out,
131
+ onCollectMissingPackage: (pkg) => {
132
+ missingPackages.add(pkg);
133
+ },
134
+ }),
135
+ {
136
+ name: 'virtual loader',
137
+ setup(build) {
138
+ build.onResolve({ filter: /^virtual:.*/ }, (args) => {
139
+ return {
140
+ path: args.path.replace(/^virtual:/, ''),
141
+ namespace: 'virtual',
142
+ };
143
+ });
144
+ build.onLoad({ filter: /.*/, namespace: 'virtual' }, async (args) => {
145
+ const name = args.path;
146
+ // Handle virtual routes module
147
+ if (name === '__routes') {
148
+ return {
149
+ contents: `export const routes = ${JSON.stringify(otherRoutes, null, 2)};`,
150
+ loader: 'js',
151
+ };
152
+ }
153
+ const url = components[name];
154
+ const componentBreakpoints = config.componentBreakpoints?.filter((x) => x.componentName === name) || [];
155
+ const brk = breakpointSizes
156
+ .map(([name, width], index) => {
157
+ const nextWidth = breakpointSizes[index + 1]?.[1] ??
158
+ Infinity;
159
+ const matchingBreakpoint = componentBreakpoints.find((x) => x.width >= width &&
160
+ x.width < nextWidth);
161
+ if (!matchingBreakpoint) {
162
+ return [];
163
+ }
164
+ return [name, matchingBreakpoint.variantId];
165
+ })
166
+ .filter((x) => x.length);
167
+ const firstVariantId = brk.find((x) => x.length)?.[1];
168
+ const responsiveVariants = firstVariantId
169
+ ? Object.fromEntries([
170
+ ['base', firstVariantId],
171
+ ...brk.slice(1),
172
+ ])
173
+ : {};
174
+ // Use virtual routes module
175
+ const routesImportPath = 'virtual:__routes';
176
+ return {
177
+ contents: /** js **/ `
178
+ 'use client'
179
+ import { Fragment } from 'react'
180
+ import { ContextProviders } from 'unframer'
181
+ import Component from '${await resolveRedirect({
182
+ url,
183
+ signal,
184
+ })}'
185
+ import { WithFramerBreakpoints } from 'unframer'
186
+ import { routes } from '${routesImportPath}'
187
+ const locales = ${JSON.stringify(config.locales) || '[]'}
188
+ const defaultResponsiveVariants = ${JSON.stringify(responsiveVariants, null, 2)}
189
+
190
+
191
+ function ComponentWithRoot({ locale, ...rest }) {
192
+ return (
193
+ <ContextProviders
194
+ routes={routes}
195
+ children={<Component {...rest} />}
196
+ framerSiteId={${JSON.stringify(config.fullFramerProjectId)}}
197
+ locale={locale}
198
+ locales={locales}
199
+ />
200
+ )
201
+ }
202
+ ComponentWithRoot.Responsive = ({ locale='', ...rest }) => {
203
+ return (
204
+ <ContextProviders
205
+ routes={routes}
206
+ children={<WithFramerBreakpoints
207
+ Component={Component}
208
+ variants={defaultResponsiveVariants}
209
+ {...rest}
210
+ />}
211
+ framerSiteId={${JSON.stringify(config.fullFramerProjectId)}}
212
+ locale={locale}
213
+ locales={locales}
214
+ />
215
+ )
216
+ }
217
+ Object.assign(ComponentWithRoot, Component)
218
+ export default ComponentWithRoot
219
+ `,
220
+ loader: 'jsx',
221
+ };
222
+ });
223
+ },
224
+ },
225
+ ],
226
+ write: false,
227
+ outdir: out,
228
+ });
229
+ const doNotEditComment = `/* This file was generated by Unframer for Framer project ${config.projectId || ''} "${config.projectName}", do not edit manually */\n`;
230
+ async function rebuild() {
231
+ // Clear missing packages for each rebuild (important for watch mode)
232
+ missingPackages.clear();
233
+ try {
234
+ const installedVersion = await resolvePackageVersion({
235
+ cwd: out,
236
+ pkg: 'unframer',
237
+ });
238
+ if (isVersionGreater(installedVersion || '0.0.0', currentUnframerVersion || '0.0.0')) {
239
+ // Version mismatch, add with specific version
240
+ missingPackages.add(`unframer@${currentUnframerVersion}`);
241
+ spinner.info(`Different unframer version detected (${installedVersion}), will install unframer@${currentUnframerVersion}`);
242
+ }
243
+ }
244
+ catch (e) {
245
+ // Unframer not installed, add with specific version
246
+ missingPackages.add(`unframer@${currentUnframerVersion}`);
247
+ spinner.info(`Missing package detected: unframer@${currentUnframerVersion}`);
248
+ }
249
+ const prevFiles = await recursiveReaddir(out);
250
+ const buildResult = await buildContext.rebuild().catch((e) => {
251
+ if (e.message.includes('No matching export ')) {
252
+ 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.`);
253
+ spinner.error(`Use --external to make all npm packagess external, then install the right version`);
254
+ process.exit(1);
255
+ }
256
+ throw e;
257
+ });
258
+ spinner.update('Finished build');
259
+ // Install missing packages if any were collected
260
+ if (missingPackages.size > 0) {
261
+ const packagesToInstall = Array.from(missingPackages);
262
+ logger.log(`Installing missing packages: ${packagesToInstall.join(', ')}`);
263
+ const installResult = await installPackagesBatch({
264
+ packageNames: packagesToInstall,
265
+ cwd: out,
266
+ isDev: false,
267
+ });
268
+ if (!installResult.success) {
269
+ spinner.error(`Failed to install packages: ${installResult.error}`);
270
+ // Don't fail the build, just warn
271
+ }
272
+ }
273
+ // First, write raw JS files for type extraction with temp prefix
274
+ for (let file of buildResult.outputFiles) {
275
+ const paths = getFilePaths(file.path, out);
276
+ const prefix = `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment;
277
+ const codeJs = prefix + file.text;
278
+ logger.log(`writing temp JS`, path.relative(out, paths.tempJsPath));
279
+ await fs.promises.mkdir(path.dirname(paths.tempJsPath), {
280
+ recursive: true,
281
+ });
282
+ await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8');
283
+ }
284
+ if (!buildResult?.outputFiles) {
285
+ throw new Error('Failed to generate result');
286
+ }
287
+ const packageJson = path.resolve(out, 'package.json');
288
+ await fs.promises.writeFile(packageJson, JSON.stringify({ type: 'module' }), 'utf-8');
289
+ const sema = new Sema(stackblitzDemoExample ? 5 : 6);
290
+ spinner.update('Extracting types');
291
+ logger.log(`using node path`, nodePath);
292
+ let allFonts = [];
293
+ const propControlsData = await Promise.all(buildResult?.outputFiles.map(async (file) => {
294
+ try {
295
+ await sema.acquire();
296
+ const name = path
297
+ .relative(out, file.path)
298
+ .replace(/\.jsx?$/, '');
299
+ const paths = getFilePaths(file.path, out);
300
+ const resultPathAbs = paths.tempJsPath;
301
+ if (!components[name]) {
302
+ return;
303
+ }
304
+ if (!fs.existsSync(resultPathAbs)) {
305
+ spinner.error(`cannot extract types for ${name}, missing output file`);
306
+ return;
307
+ }
308
+ // Only use cache in watch mode
309
+ if (watch) {
310
+ // Check if we have a cached result for this file
311
+ const currentHash = await getFileHash(resultPathAbs);
312
+ const cacheKey = resultPathAbs;
313
+ const cachedEntry = componentTypeCache.get(cacheKey);
314
+ if (cachedEntry && cachedEntry.hash === currentHash) {
315
+ // File hasn't changed, use cached results
316
+ logger.log(`using cached types for ${name}`);
317
+ // Add cached fonts to allFonts
318
+ if (cachedEntry.fonts) {
319
+ allFonts.push(...cachedEntry.fonts.map((x) => ({
320
+ ...x,
321
+ fileName: path.basename(file.path),
322
+ })));
323
+ }
324
+ return {
325
+ propertyControls: cachedEntry.propertyControls,
326
+ fonts: cachedEntry.fonts,
327
+ name,
328
+ typedocComments: cachedEntry.typedocComments,
329
+ };
330
+ }
331
+ }
332
+ // File has changed or not in cache, extract types
333
+ logger.log(`extracting types for ${name}`);
334
+ spinner.info(`Extracting types for component: ${name}`);
335
+ spinner.update(`Extracting types for ${name}`);
336
+ const { propertyControls, fonts } = await extractPropControlsUnsafe(resultPathAbs, name);
337
+ if (!propertyControls) {
338
+ logger.log(`no property controls found for ${name}`);
339
+ }
340
+ allFonts.push(...(fonts || []).map((x) => ({
341
+ ...x,
342
+ fileName: path.basename(file.path),
343
+ })));
344
+ const componentImportedName = getImportedName(name);
345
+ const typedocComments = propControlsToTypedocComments({
346
+ propertyControls: propertyControls,
347
+ logger,
348
+ componentImportedName,
349
+ locales: config.locales,
350
+ includeLocaleTypes: true,
351
+ });
352
+ logger.log(`Generated TypeDoc comments for ${name}: ${!!typedocComments.headerComment}`);
353
+ // Update cache (only if in watch mode)
354
+ if (watch) {
355
+ const currentHash = await getFileHash(resultPathAbs);
356
+ const cacheKey = resultPathAbs;
357
+ componentTypeCache.set(cacheKey, {
358
+ hash: currentHash,
359
+ propertyControls,
360
+ fonts,
361
+ typedocComments,
362
+ });
363
+ }
364
+ return {
365
+ propertyControls,
366
+ fonts,
367
+ name,
368
+ typedocComments,
369
+ };
370
+ }
371
+ catch (e) {
372
+ const name = path
373
+ .relative(out, file.path)
374
+ .replace(/\.jsx?$/, '');
375
+ spinner.error(`Failed to extract types for ${name}: ${e instanceof Error ? e.message : e}`);
376
+ logger.log(e instanceof Error ? e.stack : e);
377
+ return undefined;
378
+ }
379
+ finally {
380
+ sema.release();
381
+ }
382
+ })).finally(async () => {
383
+ try {
384
+ await fs.promises.rm(packageJson);
385
+ }
386
+ catch (error) {
387
+ // Ignore error if file doesn't exist or can't be deleted
388
+ }
389
+ });
390
+ const cssString = doNotEditComment +
391
+ '/* This css file has all the necessary styles to run all your Framer components */\n' +
392
+ '@import "unframer/styles/reset.css";\n' +
393
+ '@import "unframer/styles/framer.css";\n\n' +
394
+ getStyleTokensCss(tokens || []) +
395
+ breakpointsStyles(breakpoints) +
396
+ '\n\n' +
397
+ getFontsStyles(allFonts);
398
+ await fs.promises.writeFile(path.resolve(out, 'styles.css'), cssString, 'utf-8');
399
+ logFontsUsage(allFonts)
400
+ ?.split('\n')
401
+ .forEach((x) => logger.log(x));
402
+ function isChunkPath(filePath) {
403
+ return (filePath.includes('/chunks/') ||
404
+ filePath.includes('\\chunks\\'));
405
+ }
406
+ const jsxFiles = buildResult.outputFiles
407
+ .filter((x) => x.path.endsWith('.js') &&
408
+ !isChunkPath(x.path) &&
409
+ fs.existsSync(getFilePaths(x.path, out).jsxPath))
410
+ .map((x) => getFilePaths(x.path, out).jsxPath);
411
+ const outFiles = buildResult.outputFiles
412
+ .map((x) => {
413
+ const paths = getFilePaths(x.path, out);
414
+ if (x.path.endsWith('.js') &&
415
+ !isChunkPath(x.path) &&
416
+ fs.existsSync(paths.jsxPath)) {
417
+ return null; // Will be handled by jsx files
418
+ }
419
+ return paths.finalJsPath;
420
+ })
421
+ .filter(Boolean)
422
+ .concat([
423
+ ...(metafile ? [path.resolve(out, 'meta.json')] : []),
424
+ path.resolve(out, 'tokens.css'),
425
+ path.resolve(out, '.cursorignore'),
426
+ path.resolve(out, 'styles.css'),
427
+ ])
428
+ .concat(jsxFiles);
429
+ const filesToDelete = prevFiles
430
+ .filter((x) => !outFiles.includes(x))
431
+ .filter((x) => !x.includes(tempJsPrefix)); // Don't delete temp files here, they're handled separately
432
+ for (let file of filesToDelete) {
433
+ logger.log('deleting', path.relative(out, file));
434
+ try {
435
+ await fs.promises.rm(file);
436
+ }
437
+ catch (error) {
438
+ // Ignore error if file doesn't exist or can't be deleted
439
+ }
440
+ }
441
+ if (metafile) {
442
+ await fs.promises.writeFile(path.resolve(out, 'meta.json'), JSON.stringify(buildResult.metafile, null, 2), 'utf-8');
443
+ }
444
+ if (signal?.aborted) {
445
+ throw new Error('aborted');
446
+ }
447
+ if (watch) {
448
+ logger.log('waiting for components or config changes');
449
+ }
450
+ const res = {
451
+ components: Object.entries(components).map(([name, v]) => {
452
+ const propControls = propControlsData.find((x) => x?.name === name);
453
+ return {
454
+ componentPathSlug: name,
455
+ url: v,
456
+ name,
457
+ componentName: getImportedName(name),
458
+ propertyControls: propControls?.propertyControls,
459
+ };
460
+ }),
461
+ };
462
+ // Process and write JSX files with TypeDoc comments
463
+ spinner.update('Processing JSX files with TypeDoc comments');
464
+ for (let file of buildResult.outputFiles) {
465
+ const paths = getFilePaths(file.path, out);
466
+ const componentName = path
467
+ .relative(out, file.path)
468
+ .replace(/\.js$/, '');
469
+ const propData = propControlsData.find((p) => p?.name === componentName);
470
+ const typedocComments = propData?.typedocComments;
471
+ logger.log(`Processing component: ${componentName}`);
472
+ spinner.update(`Processing JSX for ${componentName}`);
473
+ if (!propData) {
474
+ logger.log(` No propData found for ${componentName}`);
475
+ }
476
+ else {
477
+ logger.log(` PropData found for ${componentName}, has propertyControls: ${!!propData.propertyControls}`);
478
+ if (!typedocComments) {
479
+ logger.log(` No typedocComments for ${componentName}`);
480
+ }
481
+ else {
482
+ logger.log(` TypeDoc comments available for ${componentName}`);
483
+ }
484
+ }
485
+ const existing = await fs.promises
486
+ .readFile(paths.jsxPath, 'utf-8')
487
+ .catch(() => null);
488
+ const tooBigSize = 0.7 * 1024 * 1024;
489
+ let formatted = file.text;
490
+ let tooBig = file.text.length >= tooBigSize;
491
+ let didFormat = false;
492
+ if (config.jsx &&
493
+ !tooBig &&
494
+ !paths.tempJsPath.includes('/chunks/') &&
495
+ !paths.tempJsPath.includes('\\chunks\\')) {
496
+ try {
497
+ const plugins = [
498
+ // babelPluginDeduplicateImports,
499
+ babelPluginSuppressHydration,
500
+ babelPluginJsxTransform,
501
+ removeJsxExpressionContainer,
502
+ ];
503
+ // Add TypeDoc plugin if we have comments for this component
504
+ if (typedocComments) {
505
+ logger.log(` Adding TypeDoc plugin for ${componentName}`);
506
+ plugins.push(babelPluginTypedoc(typedocComments));
507
+ }
508
+ else {
509
+ logger.log(` No TypeDoc comments to add for ${componentName}`);
510
+ }
511
+ let res = transform(file.text || '', {
512
+ babelrc: false,
513
+ sourceType: 'module',
514
+ parserOpts: {
515
+ plugins: ['jsx'],
516
+ },
517
+ plugins,
518
+ // ast: true,
519
+ // code: false,
520
+ filename: 'x.jsx',
521
+ compact: false,
522
+ sourceMaps: false,
523
+ });
524
+ if (res?.code) {
525
+ if (!biome) {
526
+ biome = await Biome.create({
527
+ distribution: Distribution.NODE,
528
+ });
529
+ }
530
+ let result = biome.formatContent(res.code, {
531
+ filePath: 'example.jsx',
532
+ });
533
+ didFormat = true;
534
+ formatted = result.content;
535
+ }
536
+ }
537
+ catch (e) {
538
+ spinner.warn(`Babel/Biome formatting failed for ${componentName}: ${e instanceof Error ? e.message : e}`);
539
+ logger.log(e instanceof Error ? e.stack : e);
540
+ notifyError(e, 'babel transform and format');
541
+ }
542
+ }
543
+ else if (tooBig) {
544
+ const sizeMB = (file.text.length / (1024 * 1024)).toFixed(2);
545
+ spinner.warn(`Skipping JSX formatting for ${componentName} (${sizeMB} MB exceeds ${(tooBigSize / (1024 * 1024)).toFixed(2)} MB limit)`);
546
+ }
547
+ const prefix = `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment;
548
+ const codeJsx = prefix + formatted;
549
+ const codeJs = prefix + file.text;
550
+ logger.log(`writing`, path.relative(out, file.path));
551
+ await fs.promises.mkdir(path.dirname(paths.jsxPath), {
552
+ recursive: true,
553
+ });
554
+ // Always write the temp .js file for type extraction
555
+ await fs.promises.writeFile(paths.tempJsPath, codeJs, 'utf-8');
556
+ // Write .jsx file if formatting succeeded, or fall back to writing
557
+ // unformatted .js content into the .jsx path when formatting fails
558
+ // (babel error, component too big, etc.) but a stale .jsx already
559
+ // exists on disk. Without this fallback the stale .jsx keeps old
560
+ // chunk imports while the old chunks are deleted, breaking the build.
561
+ if (didFormat && codeJsx !== existing) {
562
+ await fs.promises.writeFile(paths.jsxPath, codeJsx, 'utf-8');
563
+ logger.log(`Updated JSX file for ${componentName}`);
564
+ }
565
+ else if (didFormat) {
566
+ logger.log(`JSX file unchanged for ${componentName}`);
567
+ }
568
+ else if (existing && codeJs !== existing) {
569
+ // Formatting failed but .jsx exists with stale content (e.g.
570
+ // old chunk hashes). Overwrite with unformatted JS so imports
571
+ // stay in sync with the current build's chunks.
572
+ await fs.promises.writeFile(paths.jsxPath, codeJs, 'utf-8');
573
+ logger.log(`Updated JSX file (unformatted) for ${componentName}`);
574
+ }
575
+ }
576
+ spinner.stop();
577
+ // await fs.promises.writeFile(
578
+ // path.resolve(out, '.cursorignore'),
579
+ // `**/*.js\nchunks\n`,
580
+ // 'utf-8',
581
+ // )
582
+ // Clean up temp .js files and handle prefixes
583
+ for (let file of buildResult.outputFiles) {
584
+ if (file.path.endsWith('.js')) {
585
+ const paths = getFilePaths(file.path, out);
586
+ if (!isChunkPath(paths.tempJsPath) &&
587
+ fs.existsSync(paths.jsxPath)) {
588
+ // Remove temp .js file if .jsx equivalent exists
589
+ // (never applies to chunks which stay as .js)
590
+ logger.log('removing temp JS file with JSX equivalent:', path.relative(out, paths.tempJsPath));
591
+ try {
592
+ await fs.promises.rm(paths.tempJsPath);
593
+ await fs.promises.rm(paths.finalJsPath);
594
+ }
595
+ catch (error) {
596
+ // Ignore error if file doesn't exist
597
+ }
598
+ }
599
+ else {
600
+ // Rename temp .js file to final name if no .jsx equivalent
601
+ logger.log('renaming temp JS file to final name:', path.relative(out, paths.tempJsPath), '->', path.relative(out, paths.finalJsPath));
602
+ try {
603
+ await fs.promises.rename(paths.tempJsPath, paths.finalJsPath);
604
+ }
605
+ catch (error) {
606
+ // Ignore error if file doesn't exist
607
+ }
608
+ }
609
+ }
610
+ }
611
+ spinner.info(`Build completed`);
612
+ return res;
613
+ }
614
+ // when user press ctrl+c dispose
615
+ process.on('SIGINT', async () => {
616
+ spinner.stop();
617
+ console.log();
618
+ await buildContext.cancel();
619
+ buildContext.dispose();
620
+ process.exit(0); // Ensure process exits
621
+ });
622
+ process.on('SIGABRT', async () => {
623
+ spinner.stop();
624
+ console.log();
625
+ await buildContext.cancel();
626
+ buildContext.dispose();
627
+ process.exit(0); // Ensure process exits
628
+ });
629
+ signal?.addEventListener('abort', async () => {
630
+ await buildContext.cancel();
631
+ buildContext.dispose();
632
+ });
633
+ const result = await rebuild();
634
+ console.log();
635
+ console.log();
636
+ const outDirForExample = path
637
+ .relative(process.cwd(), out)
638
+ .split(path.sep)
639
+ .join('/')
640
+ .replace(/^src\//, '') || 'framer'; // remove src so file works inside src
641
+ const { exampleCode } = await createExampleComponentCode({
642
+ outDir: out,
643
+ // buildResult: result,
644
+ config,
645
+ });
646
+ if (stackblitzDemoExample) {
647
+ logger.log(`Inside Stackblitz demo, writing App.tsx`);
648
+ await fs.promises.mkdir(path.dirname(stackblitzDemoExample), {
649
+ recursive: true,
650
+ });
651
+ await fs.promises.writeFile(stackblitzDemoExample, exampleCode);
652
+ }
653
+ // Build the colorful output message
654
+ console.log();
655
+ console.log(pico.cyan(pico.bold('How to use the Framer components')));
656
+ console.log();
657
+ console.log(pico.green('✓') +
658
+ ' Your components are exported to ' +
659
+ pico.yellow(outDirForExample) +
660
+ ' folder');
661
+ console.log();
662
+ console.log(pico.dim('Each component has a ') +
663
+ pico.cyan('.Responsive') +
664
+ pico.dim(' variant that allows you to'));
665
+ console.log(pico.dim('specify different variants for different breakpoints.'));
666
+ if (exampleCode) {
667
+ console.log();
668
+ console.log(pico.magenta('You can use the components like this:'));
669
+ console.log(pico.dim('(try copy pasting the code below into your React app)'));
670
+ console.log();
671
+ console.log(exampleCode);
672
+ }
673
+ console.log();
674
+ console.log(pico.yellow('⚠') +
675
+ ' Remember to import the ' +
676
+ pico.cyan('styles.css') +
677
+ ' file to include');
678
+ console.log(' the necessary styles for the components.');
679
+ console.log();
680
+ console.log(pico.dim('To style components you can pass a ') +
681
+ pico.cyan('style') +
682
+ pico.dim(' or ') +
683
+ pico.cyan('className') +
684
+ pico.dim(' prop'));
685
+ console.log();
686
+ console.log(pico.dim('Read more on GitHub: ') +
687
+ pico.underline(pico.blue('https://github.com/remorses/unframer')));
688
+ console.log();
689
+ return { result, rebuild, buildContext };
690
+ }
691
+ const packageVersionCache = new Map();
692
+ export function resolvePackageVersion({ cwd, pkg }) {
693
+ if (packageVersionCache.has(pkg)) {
694
+ return Promise.resolve(packageVersionCache.get(pkg));
695
+ }
696
+ return new Promise((resolve, reject) => {
697
+ const code = `import('${pkg}/package.json', { with: { type: 'json' } }).then(pkg => console.log(pkg.version || pkg.default?.version));`;
698
+ const command = [
699
+ JSON.stringify(nodePath),
700
+ '-e',
701
+ JSON.stringify(code),
702
+ ].join(' ');
703
+ exec(command, {
704
+ cwd,
705
+ }, (error, stdout, stderr) => {
706
+ if (error) {
707
+ // Package not installed - this is expected and handled by auto-install
708
+ reject(new Error(`${pkg} is not installed in your project`));
709
+ return;
710
+ }
711
+ const version = stdout.trim();
712
+ packageVersionCache.set(pkg, version);
713
+ resolve(version);
714
+ });
715
+ });
716
+ }
717
+ export function resolvePackage({ cwd, pkg }) {
718
+ return new Promise((resolve) => {
719
+ const code = `import('${pkg}/package.json', { with: { type: 'json' } }).then(()=>console.log('true')).catch(()=>import('${pkg}').then(()=>console.log('true')).catch(()=>console.log('false')));`;
720
+ const command = [
721
+ JSON.stringify(nodePath),
722
+ '-e',
723
+ JSON.stringify(code),
724
+ ].join(' ');
725
+ exec(command, {
726
+ cwd,
727
+ }, (error, stdout) => {
728
+ if (error) {
729
+ resolve(false);
730
+ return;
731
+ }
732
+ const exists = stdout.trim().split('\n').pop() === 'true';
733
+ resolve(exists);
734
+ });
735
+ });
736
+ }
737
+ export function getDarkModeSelector(opts) {
738
+ const { darkModeType = 'class', content } = opts;
739
+ if (darkModeType === 'media') {
740
+ return ('@media (prefers-color-scheme: dark) {\n' +
741
+ ' :root {\n' +
742
+ content +
743
+ '\n' +
744
+ ' }\n' +
745
+ '}');
746
+ }
747
+ return '.dark {\n' + content + '\n' + '}';
748
+ }
749
+ // A CSS custom property value must not contain structural characters that would
750
+ // terminate the declaration or the surrounding `:root {}` block. Framer color
751
+ // styles can in rare cases carry malformed color strings (e.g. text scraped from
752
+ // a code-highlighting component), and an unescaped `}` closes the block early.
753
+ // The leaked text then becomes a top-level class rule like `.[-:=] { -: =; }`,
754
+ // which browsers tolerate but lightningcss (Vite's css minifier) rejects with
755
+ // "Expected identifier in class selector". Skip such tokens to keep styles.css
756
+ // always minifiable. See the getStyleTokensCss regression test.
757
+ function isSafeCssColorValue(value) {
758
+ if (typeof value !== 'string' || value === '') {
759
+ return false;
760
+ }
761
+ return !/[;{}\n\r]/.test(value);
762
+ }
763
+ export function getStyleTokensCss(tokens, darkModeType = 'class') {
764
+ if (!tokens?.length) {
765
+ return '';
766
+ }
767
+ // Drop tokens whose color values would produce invalid CSS. Both light and
768
+ // dark colors must be safe, otherwise the token is skipped entirely so the
769
+ // --unframer-* and --token-* variables never desync.
770
+ const validTokens = tokens.filter((token) => {
771
+ if (!isSafeCssColorValue(token.lightColor) ||
772
+ !isSafeCssColorValue(token.darkColor)) {
773
+ logger.log(`Skipping style token ${JSON.stringify(token.name || token.id)}: unsafe color value`);
774
+ return false;
775
+ }
776
+ return true;
777
+ });
778
+ if (!validTokens.length) {
779
+ return '';
780
+ }
781
+ // Deduplicate --unframer-* names: multiple Framer tokens can share the same
782
+ // user-facing name (e.g. two tokens both called "black" with different UUIDs).
783
+ // Keep only the first occurrence of each kebab-cased name to avoid silent
784
+ // CSS overwrites. The --token-{uuid} variables are always unique and unaffected.
785
+ const seenNames = new Set();
786
+ const deduplicatedTokens = validTokens.filter((token) => {
787
+ const name = kebabCase(token.name || token.id);
788
+ if (seenNames.has(name)) {
789
+ return false;
790
+ }
791
+ seenNames.add(name);
792
+ return true;
793
+ });
794
+ const lightUnframerTokens = deduplicatedTokens
795
+ .map((token) => ' --unframer-' +
796
+ kebabCase(token.name || token.id) +
797
+ ': ' +
798
+ token.lightColor +
799
+ ';')
800
+ .join('\n');
801
+ const lightTokens = validTokens
802
+ .map((token) => ' --token-' + token.id + ': ' + token.lightColor + ';')
803
+ .join('\n');
804
+ const darkUnframerTokens = deduplicatedTokens
805
+ .map((token) => ' --unframer-' +
806
+ kebabCase(token.name || token.id) +
807
+ ': ' +
808
+ token.darkColor +
809
+ ';')
810
+ .join('\n');
811
+ const darkTokens = validTokens
812
+ .map((token) => ' --token-' + token.id + ': ' + token.darkColor + ';')
813
+ .join('\n');
814
+ return (':root {\n' +
815
+ lightUnframerTokens +
816
+ '\n\n' +
817
+ lightTokens +
818
+ '\n' +
819
+ '}\n\n' +
820
+ getDarkModeSelector({
821
+ darkModeType,
822
+ content: darkUnframerTokens + '\n\n' + darkTokens,
823
+ }));
824
+ }
825
+ export function findRelativeLinks(text) {
826
+ const regex = /webPageId:\s+/g;
827
+ const lines = text.split('\n');
828
+ const lineNumbers = new Set();
829
+ for (let i = 0; i < lines.length; i++) {
830
+ if (regex.test(lines[i])) {
831
+ lineNumbers.add(i);
832
+ }
833
+ }
834
+ return [...lineNumbers];
835
+ }
836
+ const nodePath = process.argv[0] || 'node';
837
+ export async function extractPropControlsUnsafe(filename, name) {
838
+ const delimiter = '__delimiter__';
839
+ let propCode = `JSON.stringify({propertyControls: x.default?.propertyControls, fonts: x?.default?.fonts } || {}, null, 2)`;
840
+ const fileUrl = url.pathToFileURL(filename).href;
841
+ const code = `import('${fileUrl}').then(x => { console.log('${delimiter}'); console.log(${propCode}) })`;
842
+ const TIMEOUT = 5 * 1000;
843
+ const UNFRAMER_MAP_PACKAGES = {
844
+ unframer: url.pathToFileURL(require.resolve('../dist/index.js')).href,
845
+ react: url.pathToFileURL(require.resolve('react')).href,
846
+ 'react-dom': url.pathToFileURL(require.resolve('react-dom')).href,
847
+ 'react/jsx-runtime': url.pathToFileURL(require.resolve('react/jsx-runtime')).href,
848
+ };
849
+ let loaderOption = `--loader "${url.pathToFileURL(require.resolve('../dist/unframer-loader.js'))}"`;
850
+ // try {
851
+ // require.resolve('unframer/package.json')
852
+ // UNFRAMER_MAP_PACKAGES.unframer = ''
853
+ // } catch {}
854
+ let stdout = await new Promise((res, rej) => {
855
+ const cmd = `"${nodePath}" --no-warnings --input-type=module ${loaderOption} -e "${code}"`;
856
+ let childProcess = exec(cmd, {
857
+ env: {
858
+ // ...process.env,
859
+ UNFRAMER_MAP_PACKAGES: JSON.stringify(UNFRAMER_MAP_PACKAGES),
860
+ },
861
+ }, (err, stdout, stderr) => {
862
+ clearTimeout(timer);
863
+ if (err) {
864
+ spinner.error(`error extracting types for ${name}`);
865
+ console.error(stderr);
866
+ return rej(err);
867
+ }
868
+ res(stdout.toString());
869
+ });
870
+ const timer = setTimeout(() => {
871
+ childProcess.kill();
872
+ rej(new Error(`Timed out after ${TIMEOUT}ms while extracting types for ${name}`));
873
+ }, TIMEOUT);
874
+ }).catch((e) => {
875
+ logger.log(e.stack);
876
+ return '';
877
+ });
878
+ stdout = stdout.split(delimiter)[1] || '';
879
+ if (!stdout) {
880
+ return {};
881
+ }
882
+ // console.log(stdout)
883
+ return safeJsonParse(stdout) || {};
884
+ }
885
+ function safeJsonParse(text) {
886
+ try {
887
+ return JSON.parse(text);
888
+ }
889
+ catch (e) {
890
+ logger.error('cannot parse json', text.slice(0, 100));
891
+ return null;
892
+ }
893
+ }
894
+ export function parsePropertyControls(code) {
895
+ const start = code.indexOf('addPropertyControls(');
896
+ if (start === -1) {
897
+ logger.error('no addPropertyControls call found');
898
+ return null;
899
+ }
900
+ // count all parentheses to find when the addPropertyControls ends
901
+ let openParentheses = 0;
902
+ let closedParentheses = 0;
903
+ let current = start;
904
+ // parses using parentheses
905
+ while (current < code.length) {
906
+ const newP = code.indexOf('(', current);
907
+ const newC = code.indexOf(')', current);
908
+ if (newP === -1 && newC === -1) {
909
+ break;
910
+ }
911
+ if (newP !== -1 && newP < newC) {
912
+ openParentheses++;
913
+ current = newP + 1;
914
+ }
915
+ if (newC !== -1 && newC < newP) {
916
+ closedParentheses++;
917
+ current = newC + 1;
918
+ }
919
+ if (openParentheses === closedParentheses) {
920
+ break;
921
+ }
922
+ }
923
+ const end = current;
924
+ const propControls = code.substring(start, end);
925
+ const realStart = propControls.indexOf(',');
926
+ if (realStart === -1) {
927
+ return '';
928
+ }
929
+ return propControls.slice(realStart + 1, -1);
930
+ }
931
+ export function extractTokenInfo(code) {
932
+ const lines = code.split('\n');
933
+ const tokenLines = lines.filter((line) => line.includes('var(--token'));
934
+ const tokens = [];
935
+ for (const line of tokenLines) {
936
+ let startIndex = 0;
937
+ while (startIndex < line.length) {
938
+ startIndex = line.indexOf('var(--', startIndex);
939
+ if (startIndex === -1) {
940
+ break;
941
+ }
942
+ let parCount = 0;
943
+ let varStatement = '';
944
+ for (let i = startIndex + 3; i < line.length; i++) {
945
+ if (line[i] === '(') {
946
+ parCount++;
947
+ }
948
+ else if (line[i] === ')') {
949
+ parCount--;
950
+ }
951
+ varStatement += line[i];
952
+ if (parCount === 0) {
953
+ break;
954
+ }
955
+ }
956
+ varStatement = varStatement.trim().slice(1).slice(0, -1); // Remove starting and closing parenthesis
957
+ const [tokenName, defaultValue] = splitOnce(varStatement, ',');
958
+ let metadata;
959
+ const jsonStartIndex = line.indexOf('/*', startIndex);
960
+ if (jsonStartIndex !== -1) {
961
+ const jsonEndIndex = line.indexOf('*/', jsonStartIndex);
962
+ if (jsonEndIndex !== -1) {
963
+ const jsonString = line
964
+ .slice(jsonStartIndex + 2, jsonEndIndex)
965
+ .trim();
966
+ try {
967
+ metadata = JSON.parse(jsonString);
968
+ }
969
+ catch (error) {
970
+ // console.warn('Failed to parse JSON metadata:', error)
971
+ }
972
+ }
973
+ }
974
+ if (tokenName && defaultValue) {
975
+ tokens.push({
976
+ tokenName,
977
+ defaultValue: defaultValue.trim(),
978
+ metadata,
979
+ });
980
+ }
981
+ startIndex += varStatement.length + 'var(--'.length; // Move the startIndex to the end of the current var(--token)
982
+ }
983
+ }
984
+ return tokens;
985
+ }
986
+ function splitOnce(str, separator) {
987
+ const index = str.indexOf(separator);
988
+ if (index === -1) {
989
+ return [str];
990
+ }
991
+ return [str.slice(0, index), str.slice(index + 1)];
992
+ }
993
+ const breakpointVariants = ['mobile', 'tablet', 'desktop'];
994
+ async function recursiveReaddir(dir) {
995
+ const dirents = await fs.promises.readdir(dir, { withFileTypes: true });
996
+ const files = await Promise.all(dirents.map((dirent) => {
997
+ const res = path.resolve(dir, dirent.name);
998
+ return dirent.isDirectory() ? recursiveReaddir(res) : res;
999
+ }));
1000
+ return files.flat();
1001
+ }
1002
+ export function indentWithTabs(str, tabs) {
1003
+ if (!str)
1004
+ return '';
1005
+ return str
1006
+ .split('\n')
1007
+ .map((line, i) => (!i ? line : tabs + line))
1008
+ .join('\n');
1009
+ }
1010
+ export async function createExampleComponentCode({ outDir, config, }) {
1011
+ const outDirForExample = path
1012
+ .relative(process.cwd(), outDir)
1013
+ .split(path.sep)
1014
+ .join('/')
1015
+ .replace(/^src\//, ''); // remove src so file works inside src
1016
+ const instances = config?.componentInstancesInIndexPage?.sort((a, b) => {
1017
+ // Order first by nodeDepth (lower is better)
1018
+ return a.nodeDepth - b.nodeDepth || a.pageOrdering - b.pageOrdering;
1019
+ });
1020
+ const getImportedName = createComponentImportedNameResolver(Object.keys(config.components || {}));
1021
+ // If no instances, use components directly
1022
+ if (!instances?.length) {
1023
+ const componentNames = Object.keys(config.components || {});
1024
+ if (!componentNames.length) {
1025
+ return { outDirForExample, exampleCode: '' };
1026
+ }
1027
+ // Generate simple example with all available components
1028
+ const imports = componentNames.map((name) => {
1029
+ return `import ${getImportedName(name)} from './${outDirForExample}/${name}'`;
1030
+ });
1031
+ const jsx = componentNames.map((name) => {
1032
+ return `<${getImportedName(name)}.Responsive />`;
1033
+ });
1034
+ let containerClasses = '';
1035
+ if (config.pageBackgroundColor) {
1036
+ let bg = config.pageBackgroundColor?.replace(/ /g, '_');
1037
+ containerClasses += `bg-[${bg}]`;
1038
+ }
1039
+ const exampleCode = dedent `
1040
+ import './${outDirForExample}/styles.css'
1041
+
1042
+ ${indentWithTabs(imports?.join('\n'), '')}
1043
+
1044
+ export default function App() {
1045
+ return (
1046
+ <div className='flex flex-col items-center gap-3 ${containerClasses}'>
1047
+ ${indentWithTabs(jsx?.join('\n'), ' ')}
1048
+ </div>
1049
+ );
1050
+ };
1051
+ `;
1052
+ return {
1053
+ outDirForExample,
1054
+ exampleCode,
1055
+ };
1056
+ }
1057
+ // Deduplicate imports by componentPathSlug — the same component can be
1058
+ // used multiple times on a page, but must only be imported once, otherwise
1059
+ // the generated example code has duplicate identifier errors (e.g.
1060
+ // "Identifier `Image3FramerComponent` has already been declared").
1061
+ const seenImportSlugs = new Set();
1062
+ const imports = instances
1063
+ ?.filter((exampleComponent) => {
1064
+ const slug = exampleComponent?.componentPathSlug;
1065
+ if (!slug || seenImportSlugs.has(slug)) {
1066
+ return false;
1067
+ }
1068
+ seenImportSlugs.add(slug);
1069
+ return true;
1070
+ })
1071
+ .map((exampleComponent) => {
1072
+ return `import ${getImportedName(exampleComponent?.componentPathSlug)} from './${outDirForExample}/${exampleComponent?.componentPathSlug}'`;
1073
+ });
1074
+ const jsx = instances?.map((exampleComponent) => {
1075
+ let propStr = '';
1076
+ for (let [key, value] of Object.entries(exampleComponent.controls || {})) {
1077
+ if (key === 'variant') {
1078
+ continue;
1079
+ }
1080
+ if (typeof value === 'object') {
1081
+ continue;
1082
+ }
1083
+ // TODO get property controls to render enums much better? maybe do this in plugin instead
1084
+ propStr += '\n';
1085
+ propStr += ` ${key}={${JSON.stringify(value)}}`;
1086
+ }
1087
+ if (propStr)
1088
+ propStr += '\n';
1089
+ const responsiveComponent = `<${getImportedName(exampleComponent?.componentPathSlug)}.Responsive${propStr}/>`;
1090
+ return responsiveComponent;
1091
+ });
1092
+ if (!jsx.join().trim())
1093
+ return { outDirForExample, exampleCode: '' };
1094
+ let containerClasses = '';
1095
+ if (config.pageBackgroundColor) {
1096
+ let bg = config.pageBackgroundColor?.replace(/ /g, '_');
1097
+ containerClasses += `bg-[${bg}]`;
1098
+ }
1099
+ const exampleCode = dedent `
1100
+ import './${outDirForExample}/styles.css'
1101
+
1102
+ ${indentWithTabs(imports?.join('\n'), '')}
1103
+
1104
+ export default function App() {
1105
+ return (
1106
+ <div className='flex flex-col items-center gap-3 ${containerClasses}'>
1107
+ ${indentWithTabs(jsx?.join('\n'), ' ')}
1108
+ </div>
1109
+ );
1110
+ };
1111
+ `;
1112
+ return {
1113
+ outDirForExample,
1114
+ exampleCode,
1115
+ };
1116
+ }
1117
+ /**
1118
+ * Compares two semantic version strings.
1119
+ * Returns true if versionB is greater than versionA.
1120
+ * Handles x.y.z, x.y, x, and optional pre-release (-alpha, etc).
1121
+ */
1122
+ export function isVersionGreater(versionA, versionB) {
1123
+ try {
1124
+ function parseVersion(version) {
1125
+ // Remove pre-release (e.g. -alpha.1)
1126
+ let [core] = version.trim().split('-');
1127
+ return core.split('.').map((x) => parseInt(x, 10));
1128
+ }
1129
+ const [a1 = 0, a2 = 0, a3 = 0] = parseVersion(versionA);
1130
+ const [b1 = 0, b2 = 0, b3 = 0] = parseVersion(versionB);
1131
+ if (b1 > a1)
1132
+ return true;
1133
+ if (b1 < a1)
1134
+ return false;
1135
+ if (b2 > a2)
1136
+ return true;
1137
+ if (b2 < a2)
1138
+ return false;
1139
+ if (b3 > a3)
1140
+ return true;
1141
+ if (b3 < a3)
1142
+ return false;
1143
+ // If all equal, not greater
1144
+ return false;
1145
+ }
1146
+ catch (error) {
1147
+ spinner.error(`Error comparing versions "${versionA}" and "${versionB}": ${error?.stack || error?.message || error}`);
1148
+ return true;
1149
+ }
1150
+ }
1151
+ //# sourceMappingURL=exporter.js.map