unframer 0.6.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 +138 -0
- package/bin.mjs +4 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +98 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +2 -0
- package/dist/cli.test.d.ts.map +1 -0
- package/dist/cli.test.js +37 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/exporter.d.ts +21 -0
- package/dist/exporter.d.ts.map +1 -0
- package/dist/exporter.js +494 -0
- package/dist/exporter.js.map +1 -0
- package/dist/framer.d.ts +2 -0
- package/dist/framer.d.ts.map +1 -0
- package/dist/framer.js +2 -0
- package/dist/framer.js.map +1 -0
- package/dist/react.d.ts +16 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +157 -0
- package/dist/react.js.map +1 -0
- package/framer-fixed/dist/framer.d.ts +4829 -0
- package/framer-fixed/dist/framer.js +38175 -0
- package/package.json +63 -0
- package/src/cli.test.ts +52 -0
- package/src/cli.tsx +114 -0
- package/src/exporter.ts +572 -0
- package/src/framer.ts +1 -0
- package/src/react.tsx +229 -0
package/src/exporter.ts
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { Plugin, build, transform } from 'esbuild'
|
|
2
|
+
import dprint from 'dprint-node'
|
|
3
|
+
import tmp from 'tmp'
|
|
4
|
+
|
|
5
|
+
import pico from 'picocolors'
|
|
6
|
+
|
|
7
|
+
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ControlDescription,
|
|
11
|
+
ControlType,
|
|
12
|
+
PropertyControls,
|
|
13
|
+
} from '../framer-fixed/dist/framer.js'
|
|
14
|
+
import { fetch as _fetch } from 'native-fetch'
|
|
15
|
+
import fs from 'fs'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
import { execSync } from 'child_process'
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
|
20
|
+
const prefix = '[unframer]'
|
|
21
|
+
export const logger = {
|
|
22
|
+
log(...args) {
|
|
23
|
+
console.log(prefix, ...args)
|
|
24
|
+
},
|
|
25
|
+
error(...args) {
|
|
26
|
+
console.error([prefix, ...args].map((x) => pico.red(x)).join(' '))
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fetchWithRetry = retryTwice(_fetch) as typeof fetch
|
|
31
|
+
|
|
32
|
+
function validateUrl(url: string) {
|
|
33
|
+
try {
|
|
34
|
+
const u = new URL(url)
|
|
35
|
+
} catch (e) {
|
|
36
|
+
throw new Error(`Invalid URL: ${url}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function bundle({
|
|
41
|
+
cwd: out = '',
|
|
42
|
+
components = {} as Record<string, string>,
|
|
43
|
+
signal = new AbortController().signal,
|
|
44
|
+
}) {
|
|
45
|
+
out ||= path.resolve(process.cwd(), 'example')
|
|
46
|
+
out = path.resolve(out)
|
|
47
|
+
try {
|
|
48
|
+
fs.rmSync(out, { recursive: true, force: true })
|
|
49
|
+
} catch (e) {}
|
|
50
|
+
fs.mkdirSync(path.resolve(out), { recursive: true })
|
|
51
|
+
|
|
52
|
+
const result = await build({
|
|
53
|
+
// entryPoints: {
|
|
54
|
+
// index: url,
|
|
55
|
+
// },
|
|
56
|
+
|
|
57
|
+
entryPoints: Object.keys(components).map((name) => {
|
|
58
|
+
const url = components[name]
|
|
59
|
+
validateUrl(url)
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
in: `virtual:${name}`,
|
|
63
|
+
out: name,
|
|
64
|
+
}
|
|
65
|
+
}),
|
|
66
|
+
jsx: 'automatic',
|
|
67
|
+
|
|
68
|
+
bundle: true,
|
|
69
|
+
platform: 'browser',
|
|
70
|
+
metafile: true,
|
|
71
|
+
format: 'esm',
|
|
72
|
+
minify: false,
|
|
73
|
+
treeShaking: true,
|
|
74
|
+
splitting: true,
|
|
75
|
+
// splitting: true,
|
|
76
|
+
logLevel: 'error',
|
|
77
|
+
|
|
78
|
+
pure: ['addPropertyControls'],
|
|
79
|
+
external: whitelist,
|
|
80
|
+
plugins: [
|
|
81
|
+
esbuildPluginBundleDependencies({
|
|
82
|
+
signal,
|
|
83
|
+
}),
|
|
84
|
+
polyfillNode({}),
|
|
85
|
+
{
|
|
86
|
+
name: 'virtual loader',
|
|
87
|
+
setup(build) {
|
|
88
|
+
build.onResolve({ filter: /^virtual:.*/ }, (args) => {
|
|
89
|
+
return {
|
|
90
|
+
path: args.path.replace(/^virtual:/, ''),
|
|
91
|
+
namespace: 'virtual',
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
build.onLoad(
|
|
95
|
+
{ filter: /.*/, namespace: 'virtual' },
|
|
96
|
+
async (args) => {
|
|
97
|
+
const name = args.path
|
|
98
|
+
const url = components[name]
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
contents: /** js */ `'use client'
|
|
102
|
+
import Component from '${url}'
|
|
103
|
+
import { WithFramerBreakpoints } from 'unframer/dist/react'
|
|
104
|
+
Component.Responsive = (props) => {
|
|
105
|
+
return <WithFramerBreakpoints Component={Component} {...props} />
|
|
106
|
+
}
|
|
107
|
+
export default Component
|
|
108
|
+
`,
|
|
109
|
+
loader: 'jsx',
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
write: true,
|
|
117
|
+
|
|
118
|
+
// outfile: 'dist/example.js',
|
|
119
|
+
outdir: out,
|
|
120
|
+
// outfile: path.resolve(cwd, sourcefile),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
fs.writeFileSync(
|
|
124
|
+
path.resolve(out, 'meta.json'),
|
|
125
|
+
JSON.stringify(result.metafile, null, 2),
|
|
126
|
+
'utf-8',
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (signal.aborted) {
|
|
130
|
+
throw new Error('aborted')
|
|
131
|
+
}
|
|
132
|
+
// logger.log('result', result)
|
|
133
|
+
|
|
134
|
+
const files = fs.readdirSync(out)
|
|
135
|
+
for (let file of files) {
|
|
136
|
+
if (!file.endsWith('.js')) {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
const resultFile = path.resolve(out, file)
|
|
140
|
+
const output = fs.readFileSync(resultFile, 'utf-8')
|
|
141
|
+
logger.log(`formatting`, file)
|
|
142
|
+
const code = dprint.format(resultFile, output, {
|
|
143
|
+
lineWidth: 140,
|
|
144
|
+
quoteStyle: 'alwaysSingle',
|
|
145
|
+
trailingCommas: 'always',
|
|
146
|
+
semiColons: 'always',
|
|
147
|
+
})
|
|
148
|
+
fs.writeFileSync(resultFile, code, 'utf-8')
|
|
149
|
+
const name = file.replace(/\.js$/, '')
|
|
150
|
+
if (components[name]) {
|
|
151
|
+
logger.log(`extracting types for ${name}`)
|
|
152
|
+
const propControls = await extractPropControlsUnsafe(
|
|
153
|
+
resultFile,
|
|
154
|
+
name,
|
|
155
|
+
)
|
|
156
|
+
if (!propControls) {
|
|
157
|
+
logger.log(`no property controls found for ${name}`)
|
|
158
|
+
}
|
|
159
|
+
const types = propControlsToType(propControls)
|
|
160
|
+
// name = 'framer-' + name
|
|
161
|
+
// logger.log('name', name)
|
|
162
|
+
|
|
163
|
+
fs.writeFileSync(path.resolve(out, `${name}.d.ts`), types)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// TODO this is a vulnerability, i need to sandbox this somehow
|
|
168
|
+
|
|
169
|
+
// https://framer.com/m/Mega-Menu-2wT3.js@W0zNsrcZ2WAwVuzt0BCl
|
|
170
|
+
// let name = u.pathname
|
|
171
|
+
// .split('/')
|
|
172
|
+
// .slice(-1)[0]
|
|
173
|
+
// // https://regex101.com/r/8prywY/1
|
|
174
|
+
// // .replace(/-[\w\d]{4}\.js/i, '')
|
|
175
|
+
// .replace(/\.js/i, '')
|
|
176
|
+
// .replace(/@.*/, '')
|
|
177
|
+
// .toLowerCase()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function decapitalize(str: string) {
|
|
181
|
+
return str.charAt(0).toLowerCase() + str.slice(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function extractPropControlsSafe(text, name) {
|
|
185
|
+
try {
|
|
186
|
+
const propControlsCode = await parsePropertyControls(text)
|
|
187
|
+
// console.log('propControlsCode', propControlsCode)
|
|
188
|
+
const propControls: PropertyControls | undefined =
|
|
189
|
+
await Promise.resolve().then(async () => {
|
|
190
|
+
if (!propControlsCode) return
|
|
191
|
+
const ivm = require('isolated-vm')
|
|
192
|
+
const vm = new ivm.Isolate({ memoryLimit: 128 })
|
|
193
|
+
|
|
194
|
+
const context = vm.createContextSync()
|
|
195
|
+
|
|
196
|
+
const jail = context.global
|
|
197
|
+
|
|
198
|
+
let result = undefined
|
|
199
|
+
context.global.setSync('__return', (x) => {
|
|
200
|
+
result = x
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const mod = vm.compileModuleSync(`${text}`)
|
|
204
|
+
await mod.instantiateSync(context, (spec, mod) => {
|
|
205
|
+
// TODO instantiate framer, react, framer-motion etc
|
|
206
|
+
return
|
|
207
|
+
})
|
|
208
|
+
await mod.evaluate({})
|
|
209
|
+
return result
|
|
210
|
+
})
|
|
211
|
+
if (!propControls) {
|
|
212
|
+
logger.error(`no property controls found for component ${name}`)
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
return propControls
|
|
216
|
+
} catch (e: any) {
|
|
217
|
+
console.error(`Cannot get property controls for ${name}`, e.stack)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function extractPropControlsUnsafe(filename, name) {
|
|
222
|
+
const packageJson = path.resolve(path.dirname(filename), 'package.json')
|
|
223
|
+
try {
|
|
224
|
+
fs.writeFileSync(
|
|
225
|
+
packageJson,
|
|
226
|
+
JSON.stringify({ type: 'module' }),
|
|
227
|
+
'utf-8',
|
|
228
|
+
)
|
|
229
|
+
const delimiter = '__delimiter__'
|
|
230
|
+
let propCode = `JSON.stringify(x.default?.propertyControls || null, null, 2)`
|
|
231
|
+
// propCode = `x.default`
|
|
232
|
+
const code = `import(${JSON.stringify(
|
|
233
|
+
filename,
|
|
234
|
+
)}).then(x => { console.log(${JSON.stringify(
|
|
235
|
+
delimiter,
|
|
236
|
+
)}); console.log(${propCode})
|
|
237
|
+
})`
|
|
238
|
+
const res = execSync(`node --input-type=module -e '${code}'`)
|
|
239
|
+
let stdout = res.toString()
|
|
240
|
+
stdout = stdout.split(delimiter)[1]
|
|
241
|
+
// console.log(stdout)
|
|
242
|
+
return safeJsonParse(stdout)
|
|
243
|
+
} catch (e: any) {
|
|
244
|
+
console.error(`Cannot get property controls for ${name}`, e.stack)
|
|
245
|
+
} finally {
|
|
246
|
+
fs.rmSync(packageJson)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function safeJsonParse(text) {
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(text)
|
|
253
|
+
} catch (e) {
|
|
254
|
+
logger.error('cannot parse json', text.slice(0, 100))
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function propControlsToType(controls?: PropertyControls) {
|
|
260
|
+
try {
|
|
261
|
+
const types = Object.entries(controls || ({} as PropertyControls))
|
|
262
|
+
.map(([key, value]) => {
|
|
263
|
+
if (!value) {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const typescriptType = (value: ControlDescription<any>) => {
|
|
268
|
+
value.type
|
|
269
|
+
switch (value.type) {
|
|
270
|
+
case ControlType.Color:
|
|
271
|
+
return 'string'
|
|
272
|
+
case ControlType.Boolean:
|
|
273
|
+
return 'boolean'
|
|
274
|
+
case ControlType.Number:
|
|
275
|
+
return 'number'
|
|
276
|
+
case ControlType.String:
|
|
277
|
+
return 'string'
|
|
278
|
+
case ControlType.Enum: {
|
|
279
|
+
// @ts-expect-error
|
|
280
|
+
const options = value.optionTitles || value.options
|
|
281
|
+
return options.map((x) => `'${x}'`).join(' | ')
|
|
282
|
+
}
|
|
283
|
+
case ControlType.File:
|
|
284
|
+
return 'string'
|
|
285
|
+
case ControlType.Image:
|
|
286
|
+
return 'string'
|
|
287
|
+
case ControlType.ComponentInstance:
|
|
288
|
+
return 'React.ReactNode'
|
|
289
|
+
case ControlType.Array:
|
|
290
|
+
// @ts-expect-error
|
|
291
|
+
return `${typescriptType(value.control)}[]`
|
|
292
|
+
case ControlType.Object:
|
|
293
|
+
// @ts-expect-error
|
|
294
|
+
return `{${Object.entries(value.controls)
|
|
295
|
+
.map(([k, v]) => {
|
|
296
|
+
// @ts-expect-error
|
|
297
|
+
return `${k}: ${typescriptType(v)}`
|
|
298
|
+
})
|
|
299
|
+
.join(', ')}`
|
|
300
|
+
case ControlType.Date:
|
|
301
|
+
return 'string | Date'
|
|
302
|
+
case ControlType.Link:
|
|
303
|
+
return 'string'
|
|
304
|
+
case ControlType.ResponsiveImage:
|
|
305
|
+
return `{src: string, srcSet?: string, alt?: string}`
|
|
306
|
+
case ControlType.FusedNumber:
|
|
307
|
+
return 'number'
|
|
308
|
+
case ControlType.Transition:
|
|
309
|
+
return 'any'
|
|
310
|
+
case ControlType.EventHandler:
|
|
311
|
+
return 'Function'
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
let name = decapitalize(value.title || key || '')
|
|
315
|
+
if (!name) {
|
|
316
|
+
return ''
|
|
317
|
+
}
|
|
318
|
+
return ` ${JSON.stringify(name)}?: ${typescriptType(value)}`
|
|
319
|
+
})
|
|
320
|
+
.filter(Boolean)
|
|
321
|
+
.join('\n')
|
|
322
|
+
|
|
323
|
+
const defaultPropsTypes = ` children?: React.ReactNode\n style?: React.CSSProperties\n className?: string\n id?: string\n width?: any\n height?: any\n layoutId?: string\n`
|
|
324
|
+
let t = ''
|
|
325
|
+
t += 'import * as React from "react"\n'
|
|
326
|
+
t += `export interface Props {\n${defaultPropsTypes}${types}\n}\n`
|
|
327
|
+
t += `const Component = (props: Props) => any\n`
|
|
328
|
+
t += `export default Component\n`
|
|
329
|
+
t += `type Breakpoint = 'Desktop' | 'Tablet' | 'Mobile'\n`
|
|
330
|
+
t += `Component.Responsive = (props: Omit<Props, 'variant'> & {variants: Record<Breakpoint, Props['variant']>}) => any\n`
|
|
331
|
+
|
|
332
|
+
return t
|
|
333
|
+
} catch (e: any) {
|
|
334
|
+
logger.error('cannot generate types', e.stack)
|
|
335
|
+
return ''
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function parsePropertyControls(code: string) {
|
|
340
|
+
const start = code.indexOf('addPropertyControls(')
|
|
341
|
+
if (start === -1) {
|
|
342
|
+
logger.error('no addPropertyControls call found')
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
// count all parentheses to find when the addPropertyControls ends
|
|
346
|
+
let openParentheses = 0
|
|
347
|
+
let closedParentheses = 0
|
|
348
|
+
let current = start
|
|
349
|
+
// parses using parentheses
|
|
350
|
+
while (current < code.length) {
|
|
351
|
+
const newP = code.indexOf('(', current)
|
|
352
|
+
const newC = code.indexOf(')', current)
|
|
353
|
+
if (newP === -1 && newC === -1) {
|
|
354
|
+
break
|
|
355
|
+
}
|
|
356
|
+
if (newP !== -1 && newP < newC) {
|
|
357
|
+
openParentheses++
|
|
358
|
+
current = newP + 1
|
|
359
|
+
}
|
|
360
|
+
if (newC !== -1 && newC < newP) {
|
|
361
|
+
closedParentheses++
|
|
362
|
+
current = newC + 1
|
|
363
|
+
}
|
|
364
|
+
if (openParentheses === closedParentheses) {
|
|
365
|
+
break
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const end = current
|
|
370
|
+
const propControls = code.substring(start, end)
|
|
371
|
+
const realStart = propControls.indexOf(',')
|
|
372
|
+
if (realStart === -1) {
|
|
373
|
+
return ''
|
|
374
|
+
}
|
|
375
|
+
return propControls.slice(realStart + 1, -1)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const whitelist = [
|
|
379
|
+
'react',
|
|
380
|
+
'react-dom',
|
|
381
|
+
'framer',
|
|
382
|
+
'unframer',
|
|
383
|
+
'framer-motion', //
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
export function esbuildPluginBundleDependencies({
|
|
387
|
+
signal = undefined as AbortSignal | undefined,
|
|
388
|
+
}) {
|
|
389
|
+
const codeCache = new Map()
|
|
390
|
+
let redirectCache = new Map<string, Promise<string>>()
|
|
391
|
+
const plugin: Plugin = {
|
|
392
|
+
name: 'esbuild-plugin',
|
|
393
|
+
setup(build) {
|
|
394
|
+
build.onResolve({ filter: /^https?:\/\// }, (args) => {
|
|
395
|
+
const url = new URL(args.path)
|
|
396
|
+
return {
|
|
397
|
+
path: args.path,
|
|
398
|
+
external: false,
|
|
399
|
+
// sideEffects: false,
|
|
400
|
+
namespace: 'https',
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
const resolveDep = (args) => {
|
|
404
|
+
if (signal?.aborted) {
|
|
405
|
+
throw new Error('aborted')
|
|
406
|
+
}
|
|
407
|
+
if (args.path.startsWith('https://')) {
|
|
408
|
+
return {
|
|
409
|
+
path: args.path,
|
|
410
|
+
external: false,
|
|
411
|
+
// sideEffects: false,
|
|
412
|
+
namespace: 'https',
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (args.path === 'framer') {
|
|
416
|
+
return {
|
|
417
|
+
path: 'unframer/dist/framer',
|
|
418
|
+
external: true,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (
|
|
422
|
+
whitelist.some(
|
|
423
|
+
(x) => x === args.path || args.path.startsWith(x + '/'),
|
|
424
|
+
)
|
|
425
|
+
) {
|
|
426
|
+
return {
|
|
427
|
+
path: args.path,
|
|
428
|
+
external: true,
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// console.log('resolve', args.path)
|
|
433
|
+
if (args.path.startsWith('.') || args.path.startsWith('/')) {
|
|
434
|
+
const u = new URL(args.path, args.importer).toString()
|
|
435
|
+
// logger.log('resolve', u)
|
|
436
|
+
return {
|
|
437
|
+
path: u,
|
|
438
|
+
namespace: 'https',
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const url = `https://esm.sh/${args.path}`
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
path: url,
|
|
446
|
+
namespace: 'https',
|
|
447
|
+
external: false,
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// build.onResolve({ filter: /^\w/ }, resolveDep)
|
|
451
|
+
build.onResolve({ filter: /.*/, namespace: 'https' }, resolveDep)
|
|
452
|
+
build.onLoad({ filter: /.*/, namespace: 'https' }, async (args) => {
|
|
453
|
+
if (signal?.aborted) {
|
|
454
|
+
throw new Error('aborted')
|
|
455
|
+
}
|
|
456
|
+
const url = args.path
|
|
457
|
+
const u = new URL(url)
|
|
458
|
+
const resolved = await resolveRedirect(url, redirectCache)
|
|
459
|
+
if (codeCache.has(url)) {
|
|
460
|
+
const code = await codeCache.get(url)
|
|
461
|
+
return {
|
|
462
|
+
contents: code,
|
|
463
|
+
loader: 'js',
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
let loader = 'jsx' as any
|
|
467
|
+
const promise = Promise.resolve().then(async () => {
|
|
468
|
+
logger.log('fetching', url)
|
|
469
|
+
const res = await fetchWithRetry(resolved, { signal })
|
|
470
|
+
if (!res.ok) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Cannot fetch ${resolved}: ${res.status} ${res.statusText}`,
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
// console.log('type', res.headers.get('content-type'))
|
|
476
|
+
if (
|
|
477
|
+
res.headers
|
|
478
|
+
.get('content-type')
|
|
479
|
+
?.startsWith('application/json')
|
|
480
|
+
) {
|
|
481
|
+
loader = 'json'
|
|
482
|
+
return await res.text()
|
|
483
|
+
}
|
|
484
|
+
const text = await res.text()
|
|
485
|
+
|
|
486
|
+
const transformed = await transform(text, {
|
|
487
|
+
define: {
|
|
488
|
+
'import.meta.url': JSON.stringify(resolved),
|
|
489
|
+
},
|
|
490
|
+
minify: false,
|
|
491
|
+
format: 'esm',
|
|
492
|
+
jsx: 'transform',
|
|
493
|
+
logLevel: 'error',
|
|
494
|
+
loader,
|
|
495
|
+
platform: 'browser',
|
|
496
|
+
})
|
|
497
|
+
// console.log('transformed', resolved)
|
|
498
|
+
return transformed.code
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
codeCache.set(url, promise)
|
|
502
|
+
const code = await promise
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
contents: code,
|
|
506
|
+
|
|
507
|
+
loader,
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
return plugin
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export async function resolveRedirect(url?: string, redirectCache?: any) {
|
|
516
|
+
if (!url) {
|
|
517
|
+
return ''
|
|
518
|
+
}
|
|
519
|
+
url = url.toString()
|
|
520
|
+
if (redirectCache.has(url)) {
|
|
521
|
+
return await redirectCache.get(url)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const p = recursiveResolveRedirect(url)
|
|
525
|
+
redirectCache.set(url, p)
|
|
526
|
+
return await p
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export async function recursiveResolveRedirect(url?: string) {
|
|
530
|
+
if (!url) {
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let res = await fetchWithRetry(url, { redirect: 'manual', method: 'HEAD' })
|
|
535
|
+
const loc = res.headers.get('location')
|
|
536
|
+
if (res.status < 400 && res.status >= 300 && loc) {
|
|
537
|
+
// logger.log('redirect', loc)
|
|
538
|
+
return recursiveResolveRedirect(res.headers.get('location') || '')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return url
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function addExtension(p) {
|
|
545
|
+
const ext = path.extname(p)
|
|
546
|
+
logger.log('addExtension', ext)
|
|
547
|
+
if (!ext) {
|
|
548
|
+
return p + '.js'
|
|
549
|
+
}
|
|
550
|
+
if (ext.includes('@')) {
|
|
551
|
+
return p + '.js'
|
|
552
|
+
}
|
|
553
|
+
// if (!p.endsWith('.js')) {
|
|
554
|
+
// return p + '.js'
|
|
555
|
+
// }
|
|
556
|
+
return p
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function retryTwice<F extends Function>(fn: Function): Function {
|
|
560
|
+
return async (...args) => {
|
|
561
|
+
try {
|
|
562
|
+
return await fn(...args)
|
|
563
|
+
} catch (e: any) {
|
|
564
|
+
// ignore abort errors
|
|
565
|
+
if (e.name === 'AbortError') {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
logger.error('retrying', e.message)
|
|
569
|
+
return await fn(...args)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
package/src/framer.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../framer-fixed/dist/framer.js'
|