unframer 2.25.4 → 2.26.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 (119) hide show
  1. package/dist/babel-jsx.d.ts +15 -0
  2. package/dist/babel-jsx.d.ts.map +1 -0
  3. package/dist/babel-jsx.js +223 -0
  4. package/dist/babel-jsx.js.map +1 -0
  5. package/dist/babel-plugin-imports.d.ts +0 -6
  6. package/dist/babel-plugin-imports.d.ts.map +1 -1
  7. package/dist/babel-plugin-imports.js +2 -135
  8. package/dist/babel-plugin-imports.js.map +1 -1
  9. package/dist/cli.d.ts +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +31 -6
  12. package/dist/cli.js.map +1 -1
  13. package/dist/css.js +13 -13
  14. package/dist/esbuild.d.ts.map +1 -1
  15. package/dist/esbuild.js +82 -66
  16. package/dist/esbuild.js.map +1 -1
  17. package/dist/example-code.test.js +39 -39
  18. package/dist/example-code.test.js.map +1 -1
  19. package/dist/exporter.d.ts.map +1 -1
  20. package/dist/exporter.js +137 -87
  21. package/dist/exporter.js.map +1 -1
  22. package/dist/flat-cache-interceptor.d.ts +27 -0
  23. package/dist/flat-cache-interceptor.d.ts.map +1 -0
  24. package/dist/flat-cache-interceptor.js +99 -0
  25. package/dist/flat-cache-interceptor.js.map +1 -0
  26. package/dist/framer.d.ts.map +1 -1
  27. package/dist/framer.js +895 -741
  28. package/dist/framer.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/react.d.ts.map +1 -1
  32. package/dist/react.js +15 -3
  33. package/dist/react.js.map +1 -1
  34. package/dist/sentry.d.ts +1 -1
  35. package/dist/sentry.d.ts.map +1 -1
  36. package/dist/sentry.js +2 -17
  37. package/dist/sentry.js.map +1 -1
  38. package/dist/undici-dispatcher.d.ts +2 -0
  39. package/dist/undici-dispatcher.d.ts.map +1 -0
  40. package/dist/undici-dispatcher.js +13 -0
  41. package/dist/undici-dispatcher.js.map +1 -0
  42. package/dist/utils.d.ts +3 -3
  43. package/dist/utils.d.ts.map +1 -1
  44. package/dist/utils.js +4 -10
  45. package/dist/utils.js.map +1 -1
  46. package/dist/utils.test.d.ts +2 -0
  47. package/dist/utils.test.d.ts.map +1 -0
  48. package/dist/utils.test.js +143 -0
  49. package/dist/utils.test.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/esm/babel-jsx.d.ts +15 -0
  53. package/esm/babel-jsx.d.ts.map +1 -0
  54. package/esm/babel-jsx.js +219 -0
  55. package/esm/babel-jsx.js.map +1 -0
  56. package/esm/babel-plugin-imports.d.ts +0 -6
  57. package/esm/babel-plugin-imports.d.ts.map +1 -1
  58. package/esm/babel-plugin-imports.js +2 -134
  59. package/esm/babel-plugin-imports.js.map +1 -1
  60. package/esm/cli.d.ts +1 -0
  61. package/esm/cli.d.ts.map +1 -1
  62. package/esm/cli.js +31 -6
  63. package/esm/cli.js.map +1 -1
  64. package/esm/css.js +13 -13
  65. package/esm/esbuild.d.ts.map +1 -1
  66. package/esm/esbuild.js +82 -66
  67. package/esm/esbuild.js.map +1 -1
  68. package/esm/example-code.test.js +40 -40
  69. package/esm/example-code.test.js.map +1 -1
  70. package/esm/exporter.d.ts.map +1 -1
  71. package/esm/exporter.js +100 -50
  72. package/esm/exporter.js.map +1 -1
  73. package/esm/flat-cache-interceptor.d.ts +27 -0
  74. package/esm/flat-cache-interceptor.d.ts.map +1 -0
  75. package/esm/flat-cache-interceptor.js +95 -0
  76. package/esm/flat-cache-interceptor.js.map +1 -0
  77. package/esm/framer.d.ts.map +1 -1
  78. package/esm/framer.js +871 -729
  79. package/esm/framer.js.map +1 -1
  80. package/esm/index.d.ts +1 -1
  81. package/esm/index.d.ts.map +1 -1
  82. package/esm/react.d.ts.map +1 -1
  83. package/esm/react.js +15 -3
  84. package/esm/react.js.map +1 -1
  85. package/esm/sentry.d.ts +1 -1
  86. package/esm/sentry.d.ts.map +1 -1
  87. package/esm/sentry.js +2 -17
  88. package/esm/sentry.js.map +1 -1
  89. package/esm/undici-dispatcher.d.ts +2 -0
  90. package/esm/undici-dispatcher.d.ts.map +1 -0
  91. package/esm/undici-dispatcher.js +10 -0
  92. package/esm/undici-dispatcher.js.map +1 -0
  93. package/esm/utils.d.ts +3 -3
  94. package/esm/utils.d.ts.map +1 -1
  95. package/esm/utils.js +3 -9
  96. package/esm/utils.js.map +1 -1
  97. package/esm/utils.test.d.ts +2 -0
  98. package/esm/utils.test.d.ts.map +1 -0
  99. package/esm/utils.test.js +141 -0
  100. package/esm/utils.test.js.map +1 -0
  101. package/esm/version.d.ts +1 -1
  102. package/esm/version.js +1 -1
  103. package/package.json +8 -10
  104. package/src/babel-jsx.ts +277 -0
  105. package/src/babel-plugin-imports.ts +6 -169
  106. package/src/cli.ts +45 -6
  107. package/src/css.ts +13 -13
  108. package/src/esbuild.ts +93 -74
  109. package/src/example-code.test.ts +40 -41
  110. package/src/exporter.ts +124 -54
  111. package/src/flat-cache-interceptor.ts +114 -0
  112. package/src/framer.js +921 -764
  113. package/src/index.ts +1 -1
  114. package/src/react.tsx +15 -1
  115. package/src/sentry.ts +3 -22
  116. package/src/undici-dispatcher.ts +13 -0
  117. package/src/utils.test.ts +148 -0
  118. package/src/utils.ts +4 -17
  119. package/src/version.ts +1 -1
package/src/exporter.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { BuildResult, build, context, type BuildOptions } from 'esbuild'
2
+
2
3
  import packageJson from '../package.json'
3
4
 
4
5
  import url from 'url'
@@ -8,10 +9,17 @@ import { Sema } from 'async-sema'
8
9
 
9
10
  import { nodeModulesPolyfillPlugin } from 'esbuild-plugins-node-modules-polyfill'
10
11
 
12
+ import { transform } from '@babel/core'
11
13
  import { exec } from 'child_process'
14
+ import { error } from 'console'
12
15
  import fs from 'fs'
13
16
  import path from 'path'
14
- import dedent from 'string-dedent'
17
+ import { dedent } from './utils.js'
18
+ import {
19
+ babelPluginJsxTransform,
20
+ removeJsxExpressionContainer,
21
+ } from './babel-jsx.js'
22
+ import { propCamelCaseJustLikeFramer } from './compat.js'
15
23
  import {
16
24
  ComponentFontBundle,
17
25
  breakpointsStyles,
@@ -32,16 +40,18 @@ import {
32
40
  PropertyControls,
33
41
  combinedCSSRules,
34
42
  } from './framer.js'
43
+ import { notifyError } from './sentry'
35
44
  import {
36
- stackblitzDemoExample,
37
45
  kebabCase,
38
46
  logger,
39
47
  spinner,
48
+ stackblitzDemoExample,
40
49
  terminalMarkdown,
41
50
  } from './utils.js'
42
- import { error } from 'console'
43
- import { notifyError } from './sentry'
44
- import { propCamelCaseJustLikeFramer } from './compat'
51
+
52
+ import { Biome, Distribution } from '@biomejs/js-api'
53
+
54
+ let biome: Biome
45
55
 
46
56
  export type StyleToken = {
47
57
  id: string
@@ -68,7 +78,7 @@ export async function bundle({
68
78
  await fs.promises.mkdir(out, { recursive: true })
69
79
  } catch (e) {}
70
80
 
71
- spinner.start()
81
+ spinner.start('exporting components...')
72
82
 
73
83
  const otherRoutes = Object.fromEntries(
74
84
  (config.framerWebPages || []).map((page) => [
@@ -103,6 +113,7 @@ export async function bundle({
103
113
  }
104
114
  }),
105
115
  jsx: 'automatic',
116
+ // jsxFactory: '_jsx',
106
117
  bundle: true,
107
118
  platform: 'browser',
108
119
  metafile: true,
@@ -265,28 +276,57 @@ export async function bundle({
265
276
  spinner.update('Finished build')
266
277
 
267
278
  for (let file of buildResult.outputFiles!) {
268
- const resultPathAbs = path.resolve(out, file.path)
279
+ const resultPathAbsJs = path.resolve(out, file.path)
280
+ const resultPathAbsJsx = resultPathAbsJs.replace(/\.js$/, '.jsx')
281
+
269
282
  const existing = await fs.promises
270
- .readFile(file.path, 'utf-8')
283
+ .readFile(resultPathAbsJsx, 'utf-8')
271
284
  .catch(() => null)
272
- // let res = transform(file.text || '', {
273
- // babelrc: false,
274
- // sourceType: 'module',
275
- // plugins: [
276
- // babelPluginDeduplicateImports,
277
-
278
- // babelPluginJsxTransform(),
279
- // ],
280
- // filename: 'x.js',
281
- // compact: true,
282
- // sourceMaps: false,
283
- // })
284
- // let inputCode = res!.code!
285
-
286
- const tooBigSize = 1 * 1024 * 1024
285
+ const tooBigSize = 0.7 * 1024 * 1024
287
286
 
288
287
  let formatted = file.text
289
- // let tooBig = file.text.length >= tooBigSize
288
+
289
+ let tooBig = file.text.length >= tooBigSize
290
+ let didFormat = false
291
+ if (
292
+ config.jsx &&
293
+ !tooBig &&
294
+ !resultPathAbsJs.includes('/chunks/') &&
295
+ !resultPathAbsJs.includes('\\chunks\\')
296
+ ) {
297
+ try {
298
+ let res = transform(file.text || '', {
299
+ babelrc: false,
300
+ sourceType: 'module',
301
+ plugins: [
302
+ // babelPluginDeduplicateImports,
303
+ babelPluginJsxTransform,
304
+ removeJsxExpressionContainer,
305
+ ],
306
+ // ast: true,
307
+ // code: false,
308
+ filename: 'x.jsx',
309
+ compact: false,
310
+ sourceMaps: false,
311
+ })
312
+ if (res?.code) {
313
+ if (!biome) {
314
+ biome = await Biome.create({
315
+ distribution: Distribution.NODE,
316
+ })
317
+ }
318
+ let result = biome.formatContent(res.code, {
319
+ filePath: 'example.jsx',
320
+ })
321
+ didFormat = true
322
+ formatted = result.content
323
+ }
324
+ } catch (e) {
325
+ notifyError(e, 'babel transform and format')
326
+ }
327
+ }
328
+
329
+ // let inputCode = res!.code!
290
330
  // let shouldFormat = !tooBig && !file.path.includes('chunks')
291
331
  // if (shouldFormat) {
292
332
  // spinner.update(`Formatting ${path.relative(out, file.path)}`)
@@ -306,11 +346,6 @@ export async function bundle({
306
346
  // )
307
347
  // }
308
348
 
309
- let codeNew =
310
- `// @ts-nocheck\n` +
311
- `/* eslint-disable */\n` +
312
- doNotEditComment +
313
- formatted
314
349
  // if (framerWebPages?.length) {
315
350
  // codeNew = replaceWebPageIds({
316
351
  // code: codeNew,
@@ -327,14 +362,23 @@ export async function bundle({
327
362
  // })
328
363
  // }
329
364
 
330
- if (existing === codeNew) {
331
- continue
332
- }
365
+ const prefix =
366
+ `// @ts-nocheck\n` + `/* eslint-disable */\n` + doNotEditComment
367
+ const codeJsx = prefix + formatted
368
+ const codeJs = prefix + file.text
369
+ // if (existing === codeJsx) {
370
+ // continue
371
+ // }
333
372
  logger.log(`writing`, path.relative(out, file.path))
334
- await fs.promises.mkdir(path.dirname(resultPathAbs), {
373
+ await fs.promises.mkdir(path.dirname(resultPathAbsJsx), {
335
374
  recursive: true,
336
375
  })
337
- await fs.promises.writeFile(resultPathAbs, codeNew, 'utf-8')
376
+ if (codeJs !== codeJsx || !didFormat) {
377
+ await fs.promises.writeFile(resultPathAbsJs, codeJs, 'utf-8')
378
+ }
379
+ if (didFormat) {
380
+ await fs.promises.writeFile(resultPathAbsJsx, codeJsx, 'utf-8')
381
+ }
338
382
  }
339
383
  spinner.stop()
340
384
  await fs.promises.writeFile(
@@ -353,7 +397,7 @@ export async function bundle({
353
397
  'utf-8',
354
398
  )
355
399
 
356
- const sema = new Sema(stackblitzDemoExample ? 5 : 10)
400
+ const sema = new Sema(stackblitzDemoExample ? 5 : 6)
357
401
  spinner.update('Extracting types')
358
402
  logger.log(`using node path`, nodePath)
359
403
  let allFonts = [] as ComponentFontBundle[]
@@ -363,11 +407,17 @@ export async function bundle({
363
407
  await sema.acquire()
364
408
  const name = path
365
409
  .relative(out, file.path)
366
- .replace(/\.js$/, '')
410
+ .replace(/\.jsx?$/, '')
367
411
  const resultPathAbs = path.resolve(out, file.path)
368
412
  if (!components[name]) {
369
413
  return
370
414
  }
415
+ if (!fs.existsSync(resultPathAbs)) {
416
+ spinner.error(
417
+ `cannot extract types for ${name}, missing output file`,
418
+ )
419
+ return
420
+ }
371
421
  logger.log(`extracting types for ${name}`)
372
422
  spinner.update(`Extracting types for ${name}`)
373
423
  const { propertyControls, fonts } =
@@ -392,6 +442,7 @@ export async function bundle({
392
442
  path.resolve(out, `${name}.d.ts`),
393
443
  types,
394
444
  )
445
+
395
446
  return {
396
447
  propertyControls,
397
448
  fonts,
@@ -430,9 +481,16 @@ export async function bundle({
430
481
  )
431
482
 
432
483
  logFontsUsage(allFonts)
433
- .split('\n')
484
+ ?.split('\n')
434
485
  .forEach((x) => logger.log(x))
435
486
 
487
+ const jsxFiles = buildResult.outputFiles
488
+ .filter(
489
+ (x) =>
490
+ x.path.endsWith('.js') &&
491
+ fs.existsSync(x.path.replace(/\.js$/, '.jsx')),
492
+ )
493
+ .map((x) => x.path.replace(/\.js$/, '.jsx'))
436
494
  const outFiles = buildResult.outputFiles
437
495
  .map((x) => path.resolve(out, x.path))
438
496
  .concat([
@@ -441,12 +499,25 @@ export async function bundle({
441
499
  path.resolve(out, '.cursorignore'),
442
500
  path.resolve(out, 'styles.css'),
443
501
  ])
502
+ .concat(jsxFiles)
444
503
  .concat(
445
504
  buildResult.outputFiles.map((x) =>
446
- path.resolve(out, x.path.replace('.js', '.d.ts')),
505
+ path.resolve(out, x.path.replace(/\.jsx?$/, '.d.ts')),
447
506
  ),
448
507
  )
449
- const filesToDelete = prevFiles.filter((x) => !outFiles.includes(x))
508
+
509
+ const filesToDelete = prevFiles
510
+ .filter((x) => !outFiles.includes(x))
511
+ .concat(
512
+ buildResult.outputFiles
513
+ .map((x) => x.path)
514
+ .filter(
515
+ (js) =>
516
+ js.endsWith('.js') &&
517
+ jsxFiles.some((x) => x.startsWith(js)),
518
+ ),
519
+ )
520
+
450
521
  for (let file of filesToDelete) {
451
522
  logger.log('deleting', path.relative(out, file))
452
523
  try {
@@ -596,7 +667,7 @@ export function resolvePackage({ cwd, pkg }) {
596
667
  if (error) {
597
668
  logger.log(stderr)
598
669
  reject(
599
- new Error('Unframer is not installed in your project'),
670
+ new Error(`${pkg} is not installed in your project`),
600
671
  )
601
672
  return
602
673
  }
@@ -1162,7 +1233,7 @@ function splitOnce(str: string, separator: string) {
1162
1233
  }
1163
1234
 
1164
1235
  export function componentCamelCase(str: string) {
1165
- str = str?.replace(/\.js$/, '')
1236
+ str = str?.replace(/\.jsx?$/, '')
1166
1237
  if (!str) {
1167
1238
  return 'FramerComponent'
1168
1239
  }
@@ -1253,6 +1324,7 @@ async function recursiveReaddir(dir: string): Promise<string[]> {
1253
1324
  }
1254
1325
 
1255
1326
  function indentWithTabs(str: string, tabs: string) {
1327
+ if (!str) return ''
1256
1328
  return str
1257
1329
  .split('\n')
1258
1330
  .map((line, i) => (!i ? line : tabs + line))
@@ -1269,18 +1341,18 @@ export async function createExampleComponentCode({
1269
1341
  const outDirForExample = path.posix
1270
1342
  .relative(process.cwd(), outDir)
1271
1343
  .replace(/^src\//, '') // remove src so file works inside src
1272
- const instances = config?.componentInstancesInIndexPage.sort((a, b) => {
1344
+ const instances = config?.componentInstancesInIndexPage?.sort((a, b) => {
1273
1345
  // Order first by nodeDepth (lower is better)
1274
1346
  return a.nodeDepth - b.nodeDepth || a.pageOrdering - b.pageOrdering
1275
1347
  })
1276
1348
 
1277
- const imports = instances.map((exampleComponent) => {
1349
+ const imports = instances?.map((exampleComponent) => {
1278
1350
  return `import ${componentCamelCase(exampleComponent?.componentPathSlug)} from './${outDirForExample}/${
1279
1351
  exampleComponent?.componentPathSlug
1280
1352
  }'`
1281
1353
  })
1282
1354
 
1283
- const jsx = instances.map((exampleComponent) => {
1355
+ const jsx = instances?.map((exampleComponent) => {
1284
1356
  let propStr = ''
1285
1357
  for (let [key, value] of Object.entries(
1286
1358
  exampleComponent.controls || {},
@@ -1293,12 +1365,10 @@ export async function createExampleComponentCode({
1293
1365
  }
1294
1366
  // TODO get property controls to render enums much better? maybe do this in plugin instead
1295
1367
  propStr += '\n'
1296
- propStr += ` ${key}={${JSON.stringify(value)}}`
1368
+ propStr += ` ${key}={${JSON.stringify(value)}}`
1297
1369
  }
1298
1370
  if (propStr) propStr += '\n'
1299
- const responsiveComponent = dedent`
1300
- <${componentCamelCase(exampleComponent?.componentPathSlug)}.Responsive${propStr}/>
1301
- `
1371
+ const responsiveComponent = `<${componentCamelCase(exampleComponent?.componentPathSlug)}.Responsive${propStr}/>`
1302
1372
  return responsiveComponent
1303
1373
  })
1304
1374
 
@@ -1311,14 +1381,14 @@ export async function createExampleComponentCode({
1311
1381
  const exampleCode = dedent`
1312
1382
  import './${outDirForExample}/styles.css'
1313
1383
 
1314
- ${indentWithTabs(imports.join('\n'), '')}
1384
+ ${indentWithTabs(imports?.join('\n'), '')}
1315
1385
 
1316
1386
  export default function App() {
1317
- return (
1318
- <div className='flex flex-col items-center gap-3 ${containerClasses}'>
1319
- ${indentWithTabs(jsx.join('\n'), ' ')}
1320
- </div>
1321
- );
1387
+ return (
1388
+ <div className='flex flex-col items-center gap-3 ${containerClasses}'>
1389
+ ${indentWithTabs(jsx?.join('\n'), ' ')}
1390
+ </div>
1391
+ );
1322
1392
  };
1323
1393
  `
1324
1394
  return {
@@ -0,0 +1,114 @@
1
+ // FlatCacheStore.ts
2
+ import { Writable } from 'node:stream'
3
+ import { promises as fs } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { tmpdir } from 'node:os'
6
+ import { createHash } from 'node:crypto'
7
+
8
+ import type CacheHandler from 'undici/types/cache-interceptor.js'
9
+ import { logger } from './utils'
10
+
11
+ /* Narrow the names we need from the .d.ts so we stay 1-to-1 with the built-ins */
12
+ type CacheKey = CacheHandler.CacheKey
13
+ type CacheValue = CacheHandler.CacheValue
14
+ type GetResult = CacheHandler.GetResult
15
+ type CacheStore = CacheHandler.CacheStore
16
+
17
+ /**
18
+ * A CacheStore that persists each entry as separate files in os.tmpdir()/.unframer.
19
+ * Each cache entry creates a .json file for metadata and a .bin file for the body.
20
+ * It satisfies the exact same interface that `MemoryCacheStore` and
21
+ * `SqliteCacheStore` implement inside Undici (see cache-interceptor.d.ts).
22
+ */
23
+ export class FlatCacheStore implements CacheStore {
24
+ private readonly cacheDir: string
25
+
26
+ constructor() {
27
+ this.cacheDir = join(tmpdir(), '.unframer')
28
+ logger.log(`using cache dir`, this.cacheDir)
29
+ this.ensureCacheDir()
30
+ }
31
+
32
+ private async ensureCacheDir(): Promise<void> {
33
+ try {
34
+ await fs.mkdir(this.cacheDir, { recursive: true })
35
+ } catch (error) {
36
+ // Directory might already exist, ignore error
37
+ }
38
+ }
39
+
40
+ private getFileHash(key: CacheKey): string {
41
+ return createHash('sha256').update(JSON.stringify(key)).digest('hex')
42
+ }
43
+
44
+ private getFilePaths(key: CacheKey): {
45
+ metaPath: string
46
+ bodyPath: string
47
+ } {
48
+ const hash = this.getFileHash(key)
49
+ return {
50
+ metaPath: join(this.cacheDir, `${hash}.json`),
51
+ bodyPath: join(this.cacheDir, `${hash}.bin`),
52
+ }
53
+ }
54
+
55
+ /** Read a cached response (metadata + body) */
56
+ async get(key: CacheKey): Promise<GetResult | undefined> {
57
+ try {
58
+ const { metaPath, bodyPath } = this.getFilePaths(key)
59
+
60
+ const [metaData, bodyData] = await Promise.all([
61
+ fs.readFile(metaPath, 'utf-8'),
62
+ fs.readFile(bodyPath),
63
+ ])
64
+
65
+ const meta = JSON.parse(metaData)
66
+ return {
67
+ ...meta,
68
+ body: bodyData,
69
+ }
70
+ } catch (error) {
71
+ return undefined
72
+ }
73
+ }
74
+
75
+ /** Return a writable stream so the interceptor can pipe the body into us */
76
+ createWriteStream(key: CacheKey, meta: CacheValue): Writable {
77
+ const chunks: Buffer[] = []
78
+
79
+ return new Writable({
80
+ write(chunk, _enc, cb) {
81
+ chunks.push(chunk as Buffer)
82
+ cb()
83
+ },
84
+ final: async (cb) => {
85
+ try {
86
+ await this.ensureCacheDir()
87
+ const { metaPath, bodyPath } = this.getFilePaths(key)
88
+
89
+ await Promise.all([
90
+ fs.writeFile(metaPath, JSON.stringify(meta, null, 2)),
91
+ fs.writeFile(bodyPath, Buffer.concat(chunks)),
92
+ ])
93
+
94
+ cb()
95
+ } catch (error) {
96
+ cb(error as Error)
97
+ }
98
+ },
99
+ })
100
+ }
101
+
102
+ /** Delete one entry */
103
+ async delete(key: CacheKey): Promise<void> {
104
+ try {
105
+ const { metaPath, bodyPath } = this.getFilePaths(key)
106
+ await Promise.all([
107
+ fs.unlink(metaPath).catch(() => {}),
108
+ fs.unlink(bodyPath).catch(() => {}),
109
+ ])
110
+ } catch (error) {
111
+ // Ignore errors when deleting non-existent files
112
+ }
113
+ }
114
+ }