@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.
- package/README.md +389 -0
- package/bin.js +5 -0
- package/dist/babel-jsx.d.ts +24 -0
- package/dist/babel-jsx.js +299 -0
- package/dist/babel-plugin-imports.d.ts +15 -0
- package/dist/babel-plugin-imports.js +211 -0
- package/dist/babel-typedoc.d.ts +39 -0
- package/dist/babel-typedoc.js +76 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/cli.d.ts +59 -0
- package/dist/cli.js +821 -0
- package/dist/compat.d.ts +2 -0
- package/dist/compat.js +23 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +27 -0
- package/dist/css-core.d.ts +50 -0
- package/dist/css-core.js +231 -0
- package/dist/css.d.ts +15 -0
- package/dist/css.js +36 -0
- package/dist/esbuild.d.ts +31 -0
- package/dist/esbuild.js +317 -0
- package/dist/exporter.d.ts +96 -0
- package/dist/exporter.js +1151 -0
- package/dist/flat-cache-interceptor.d.ts +24 -0
- package/dist/flat-cache-interceptor.js +87 -0
- package/dist/framer-chunks/chunk-IKQSD2QC.js +10 -0
- package/dist/framer-chunks/fontshare-PZLWRK4B-MHMZIGTX.js +7 -0
- package/dist/framer-chunks/fontshare-SXU5BGFE-OWTMMPGS.js +7 -0
- package/dist/framer-chunks/fontshare-TIA7QUPT-PUDLUTQ7.js +7 -0
- package/dist/framer-chunks/framer-font-RD2SUPQH-Q4MS7WS6.js +7 -0
- package/dist/framer-chunks/google-EGNT223R-P4DUHBW2.js +7 -0
- package/dist/framer-chunks/google-H6SFY4F5-5JSJCGDR.js +7 -0
- package/dist/framer-chunks/google-YSYBFRE6-L7YAHH7V.js +7 -0
- package/dist/framer-client.server.d.ts +4 -0
- package/dist/framer-client.server.js +45 -0
- package/dist/framer-package-versions.d.ts +781 -0
- package/dist/framer-package-versions.js +782 -0
- package/dist/framer-types.d.ts +30 -0
- package/dist/framer-types.js +35 -0
- package/dist/framer.d.ts +4990 -0
- package/dist/framer.js +64296 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/package-manager.d.ts +10 -0
- package/dist/package-manager.js +138 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
- package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/cms.js +58 -0
- package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/errors.js +48 -0
- package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +3 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.js +46 -0
- package/dist/plugin-mcp-dist/lib/framer.d.ts +30 -0
- package/dist/plugin-mcp-dist/lib/framer.js +1084 -0
- package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +142 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +385 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1924 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +170 -0
- package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
- package/dist/plugin-mcp-dist/lib/react-export.js +358 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts +276 -0
- package/dist/plugin-mcp-dist/lib/schema.js +907 -0
- package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/store.js +10 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
- package/dist/plugin-mcp-dist/lib/tunnel.d.ts +46 -0
- package/dist/plugin-mcp-dist/lib/tunnel.js +117 -0
- package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
- package/dist/plugin-mcp-dist/lib/types.js +188 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/upstream-socket.js +56 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts +13 -0
- package/dist/plugin-mcp-dist/lib/utils.js +57 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
- package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/xml.js +395 -0
- package/dist/react.d.ts +76 -0
- package/dist/react.js +280 -0
- package/dist/renamer.d.ts +11 -0
- package/dist/renamer.js +139 -0
- package/dist/sentry.d.ts +2 -0
- package/dist/sentry.js +5 -0
- package/dist/stackblitz.d.ts +9 -0
- package/dist/stackblitz.js +179 -0
- package/dist/styles/framer.css +920 -0
- package/dist/styles/reset.css +37 -0
- package/dist/typescript.d.ts +24 -0
- package/dist/typescript.js +197 -0
- package/dist/undici-dispatcher.d.ts +2 -0
- package/dist/undici-dispatcher.js +10 -0
- package/dist/unframer-loader.d.ts +2 -0
- package/dist/unframer-loader.js +12 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.js +81 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +131 -0
package/dist/exporter.js
ADDED
|
@@ -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
|