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.
- package/dist/babel-jsx.d.ts +15 -0
- package/dist/babel-jsx.d.ts.map +1 -0
- package/dist/babel-jsx.js +223 -0
- package/dist/babel-jsx.js.map +1 -0
- package/dist/babel-plugin-imports.d.ts +0 -6
- package/dist/babel-plugin-imports.d.ts.map +1 -1
- package/dist/babel-plugin-imports.js +2 -135
- package/dist/babel-plugin-imports.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +31 -6
- package/dist/cli.js.map +1 -1
- package/dist/css.js +13 -13
- package/dist/esbuild.d.ts.map +1 -1
- package/dist/esbuild.js +82 -66
- package/dist/esbuild.js.map +1 -1
- package/dist/example-code.test.js +39 -39
- package/dist/example-code.test.js.map +1 -1
- package/dist/exporter.d.ts.map +1 -1
- package/dist/exporter.js +137 -87
- package/dist/exporter.js.map +1 -1
- package/dist/flat-cache-interceptor.d.ts +27 -0
- package/dist/flat-cache-interceptor.d.ts.map +1 -0
- package/dist/flat-cache-interceptor.js +99 -0
- package/dist/flat-cache-interceptor.js.map +1 -0
- package/dist/framer.d.ts.map +1 -1
- package/dist/framer.js +895 -741
- package/dist/framer.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +15 -3
- package/dist/react.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -17
- package/dist/sentry.js.map +1 -1
- package/dist/undici-dispatcher.d.ts +2 -0
- package/dist/undici-dispatcher.d.ts.map +1 -0
- package/dist/undici-dispatcher.js +13 -0
- package/dist/undici-dispatcher.js.map +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -10
- package/dist/utils.js.map +1 -1
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +143 -0
- package/dist/utils.test.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/esm/babel-jsx.d.ts +15 -0
- package/esm/babel-jsx.d.ts.map +1 -0
- package/esm/babel-jsx.js +219 -0
- package/esm/babel-jsx.js.map +1 -0
- package/esm/babel-plugin-imports.d.ts +0 -6
- package/esm/babel-plugin-imports.d.ts.map +1 -1
- package/esm/babel-plugin-imports.js +2 -134
- package/esm/babel-plugin-imports.js.map +1 -1
- package/esm/cli.d.ts +1 -0
- package/esm/cli.d.ts.map +1 -1
- package/esm/cli.js +31 -6
- package/esm/cli.js.map +1 -1
- package/esm/css.js +13 -13
- package/esm/esbuild.d.ts.map +1 -1
- package/esm/esbuild.js +82 -66
- package/esm/esbuild.js.map +1 -1
- package/esm/example-code.test.js +40 -40
- package/esm/example-code.test.js.map +1 -1
- package/esm/exporter.d.ts.map +1 -1
- package/esm/exporter.js +100 -50
- package/esm/exporter.js.map +1 -1
- package/esm/flat-cache-interceptor.d.ts +27 -0
- package/esm/flat-cache-interceptor.d.ts.map +1 -0
- package/esm/flat-cache-interceptor.js +95 -0
- package/esm/flat-cache-interceptor.js.map +1 -0
- package/esm/framer.d.ts.map +1 -1
- package/esm/framer.js +871 -729
- package/esm/framer.js.map +1 -1
- package/esm/index.d.ts +1 -1
- package/esm/index.d.ts.map +1 -1
- package/esm/react.d.ts.map +1 -1
- package/esm/react.js +15 -3
- package/esm/react.js.map +1 -1
- package/esm/sentry.d.ts +1 -1
- package/esm/sentry.d.ts.map +1 -1
- package/esm/sentry.js +2 -17
- package/esm/sentry.js.map +1 -1
- package/esm/undici-dispatcher.d.ts +2 -0
- package/esm/undici-dispatcher.d.ts.map +1 -0
- package/esm/undici-dispatcher.js +10 -0
- package/esm/undici-dispatcher.js.map +1 -0
- package/esm/utils.d.ts +3 -3
- package/esm/utils.d.ts.map +1 -1
- package/esm/utils.js +3 -9
- package/esm/utils.js.map +1 -1
- package/esm/utils.test.d.ts +2 -0
- package/esm/utils.test.d.ts.map +1 -0
- package/esm/utils.test.js +141 -0
- package/esm/utils.test.js.map +1 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/package.json +8 -10
- package/src/babel-jsx.ts +277 -0
- package/src/babel-plugin-imports.ts +6 -169
- package/src/cli.ts +45 -6
- package/src/css.ts +13 -13
- package/src/esbuild.ts +93 -74
- package/src/example-code.test.ts +40 -41
- package/src/exporter.ts +124 -54
- package/src/flat-cache-interceptor.ts +114 -0
- package/src/framer.js +921 -764
- package/src/index.ts +1 -1
- package/src/react.tsx +15 -1
- package/src/sentry.ts +3 -22
- package/src/undici-dispatcher.ts +13 -0
- package/src/utils.test.ts +148 -0
- package/src/utils.ts +4 -17
- 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 '
|
|
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
|
-
|
|
43
|
-
import {
|
|
44
|
-
|
|
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
|
|
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(
|
|
283
|
+
.readFile(resultPathAbsJsx, 'utf-8')
|
|
271
284
|
.catch(() => null)
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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(
|
|
373
|
+
await fs.promises.mkdir(path.dirname(resultPathAbsJsx), {
|
|
335
374
|
recursive: true,
|
|
336
375
|
})
|
|
337
|
-
|
|
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 :
|
|
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(/\.
|
|
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
|
-
|
|
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(
|
|
505
|
+
path.resolve(out, x.path.replace(/\.jsx?$/, '.d.ts')),
|
|
447
506
|
),
|
|
448
507
|
)
|
|
449
|
-
|
|
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(
|
|
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(/\.
|
|
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
|
|
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
|
|
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
|
|
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 += `
|
|
1368
|
+
propStr += ` ${key}={${JSON.stringify(value)}}`
|
|
1297
1369
|
}
|
|
1298
1370
|
if (propStr) propStr += '\n'
|
|
1299
|
-
const responsiveComponent =
|
|
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
|
|
1384
|
+
${indentWithTabs(imports?.join('\n'), '')}
|
|
1315
1385
|
|
|
1316
1386
|
export default function App() {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
+
}
|