@se-studio/ab-testing 0.0.0-next16-20260822103555
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/CHANGELOG.md +1125 -0
- package/README.md +412 -0
- package/dist/codegen/cli.d.ts +3 -0
- package/dist/codegen/cli.d.ts.map +1 -0
- package/dist/codegen/cli.js +121 -0
- package/dist/codegen/cli.js.map +1 -0
- package/dist/codegen/generator.d.ts +42 -0
- package/dist/codegen/generator.d.ts.map +1 -0
- package/dist/codegen/generator.js +56 -0
- package/dist/codegen/generator.js.map +1 -0
- package/dist/codegen/index.d.ts +9 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +8 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/components/AbTestReporter.d.ts +2 -0
- package/dist/components/AbTestReporter.d.ts.map +1 -0
- package/dist/components/AbTestReporter.js +19 -0
- package/dist/components/AbTestReporter.js.map +1 -0
- package/dist/components/AbTestUtmScript.d.ts +17 -0
- package/dist/components/AbTestUtmScript.d.ts.map +1 -0
- package/dist/components/AbTestUtmScript.js +43 -0
- package/dist/components/AbTestUtmScript.js.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/components/index.js.map +1 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/useAbTestAssignments.d.ts +56 -0
- package/dist/hooks/useAbTestAssignments.d.ts.map +1 -0
- package/dist/hooks/useAbTestAssignments.js +87 -0
- package/dist/hooks/useAbTestAssignments.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/assignment.d.ts +31 -0
- package/dist/middleware/assignment.d.ts.map +1 -0
- package/dist/middleware/assignment.js +125 -0
- package/dist/middleware/assignment.js.map +1 -0
- package/dist/middleware/cache.d.ts +30 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +140 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/cookies.d.ts +44 -0
- package/dist/middleware/cookies.d.ts.map +1 -0
- package/dist/middleware/cookies.js +66 -0
- package/dist/middleware/cookies.js.map +1 -0
- package/dist/middleware/handler.d.ts +53 -0
- package/dist/middleware/handler.d.ts.map +1 -0
- package/dist/middleware/handler.js +201 -0
- package/dist/middleware/handler.js.map +1 -0
- package/dist/middleware/index.d.ts +15 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +13 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/prune.d.ts +13 -0
- package/dist/middleware/prune.d.ts.map +1 -0
- package/dist/middleware/prune.js +29 -0
- package/dist/middleware/prune.js.map +1 -0
- package/dist/middleware/types.d.ts +103 -0
- package/dist/middleware/types.d.ts.map +1 -0
- package/dist/middleware/types.js +2 -0
- package/dist/middleware/types.js.map +1 -0
- package/dist/middleware/urlParams.d.ts +7 -0
- package/dist/middleware/urlParams.d.ts.map +1 -0
- package/dist/middleware/urlParams.js +20 -0
- package/dist/middleware/urlParams.js.map +1 -0
- package/dist/middleware/variantId.d.ts +9 -0
- package/dist/middleware/variantId.d.ts.map +1 -0
- package/dist/middleware/variantId.js +12 -0
- package/dist/middleware/variantId.js.map +1 -0
- package/dist/reporting/buildExperimentImpression.d.ts +6 -0
- package/dist/reporting/buildExperimentImpression.d.ts.map +1 -0
- package/dist/reporting/buildExperimentImpression.js +22 -0
- package/dist/reporting/buildExperimentImpression.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +20 -0
- package/dist/utils.js.map +1 -0
- package/dist/webhook/handler.d.ts +109 -0
- package/dist/webhook/handler.d.ts.map +1 -0
- package/dist/webhook/handler.js +128 -0
- package/dist/webhook/handler.js.map +1 -0
- package/dist/webhook/index.d.ts +8 -0
- package/dist/webhook/index.d.ts.map +1 -0
- package/dist/webhook/index.js +7 -0
- package/dist/webhook/index.js.map +1 -0
- package/docs/llms.md +106 -0
- package/package.json +91 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a single bucket config, accepting legacy field names.
|
|
3
|
+
*/
|
|
4
|
+
export function normalizeVariantConfig(raw) {
|
|
5
|
+
const reporting_event = raw.reporting_event ?? raw.hubspot_event_name;
|
|
6
|
+
const url_params = raw.url_params ??
|
|
7
|
+
(raw.utm_content !== undefined ? { utm_content: raw.utm_content } : undefined);
|
|
8
|
+
const config = {
|
|
9
|
+
weight: raw.weight,
|
|
10
|
+
};
|
|
11
|
+
if (raw.variant_label) {
|
|
12
|
+
config.variant_label = raw.variant_label;
|
|
13
|
+
}
|
|
14
|
+
if (reporting_event) {
|
|
15
|
+
config.reporting_event = reporting_event;
|
|
16
|
+
}
|
|
17
|
+
if (url_params && Object.keys(url_params).length > 0) {
|
|
18
|
+
config.url_params = url_params;
|
|
19
|
+
}
|
|
20
|
+
if (raw.reporting_params && Object.keys(raw.reporting_params).length > 0) {
|
|
21
|
+
config.reporting_params = raw.reporting_params;
|
|
22
|
+
}
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Transform raw Contentful PageTest data into AbTest format.
|
|
27
|
+
*/
|
|
28
|
+
export function transformPageTest(raw, onSkipped) {
|
|
29
|
+
const controlSlug = raw.control?.slug;
|
|
30
|
+
if (!controlSlug) {
|
|
31
|
+
onSkipped?.(raw.sys.id, 'No control slug');
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const variants = (raw.variantsCollection?.items ?? [])
|
|
35
|
+
.filter((item) => item !== null && item.slug !== null)
|
|
36
|
+
.map((item) => ({
|
|
37
|
+
id: item.sys.id,
|
|
38
|
+
slug: item.slug,
|
|
39
|
+
}));
|
|
40
|
+
const configuration = (raw.configuration ?? []).map(normalizeVariantConfig);
|
|
41
|
+
return {
|
|
42
|
+
id: raw.sys.id,
|
|
43
|
+
cmsLabel: raw.cmsLabel || '',
|
|
44
|
+
controlSlug,
|
|
45
|
+
searchParameters: raw.searchParameters ?? undefined,
|
|
46
|
+
trackingLabel: raw.trackingLabel ?? undefined,
|
|
47
|
+
enabled: raw.enabled ?? false,
|
|
48
|
+
injectUrlParams: raw.injectUrlParams ?? false,
|
|
49
|
+
configuration,
|
|
50
|
+
variants,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Process all PageTest entries and store them in the blob store.
|
|
55
|
+
*
|
|
56
|
+
* @param config - Webhook handler configuration
|
|
57
|
+
* @returns Result with success status and count
|
|
58
|
+
*/
|
|
59
|
+
export async function processAbTestWebhook(config) {
|
|
60
|
+
const { fetchPageTests, getStore, onSkippedTest, revalidate } = config;
|
|
61
|
+
try {
|
|
62
|
+
// Revalidate cache tags if function provided
|
|
63
|
+
if (revalidate) {
|
|
64
|
+
await Promise.resolve(revalidate());
|
|
65
|
+
}
|
|
66
|
+
// Fetch all active tests from Contentful
|
|
67
|
+
const rawTests = await fetchPageTests();
|
|
68
|
+
// Transform and index by control slug
|
|
69
|
+
const abTests = {};
|
|
70
|
+
for (const raw of rawTests) {
|
|
71
|
+
const test = transformPageTest(raw, onSkippedTest);
|
|
72
|
+
if (test) {
|
|
73
|
+
abTests[test.controlSlug] = test;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Store in blob store
|
|
77
|
+
const store = await Promise.resolve(getStore());
|
|
78
|
+
await store.bulkWrite(Object.entries(abTests));
|
|
79
|
+
return {
|
|
80
|
+
success: true,
|
|
81
|
+
count: Object.keys(abTests).length,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
// biome-ignore lint/suspicious/noConsole: Error logging in webhook
|
|
86
|
+
console.error('Error processing ab-test webhook:', error);
|
|
87
|
+
return {
|
|
88
|
+
success: false,
|
|
89
|
+
count: 0,
|
|
90
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a Next.js API route handler for the A/B test webhook.
|
|
96
|
+
*/
|
|
97
|
+
export function createWebhookHandler(config) {
|
|
98
|
+
return async function handler(request) {
|
|
99
|
+
// Validate webhook secret if configured
|
|
100
|
+
if (config.webhookSecret) {
|
|
101
|
+
const secret = request.headers.get('x-contentful-webhook-secret');
|
|
102
|
+
if (secret !== config.webhookSecret) {
|
|
103
|
+
return new Response(JSON.stringify({ message: 'Invalid secret' }), {
|
|
104
|
+
status: 401,
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const result = await processAbTestWebhook(config);
|
|
110
|
+
if (result.success) {
|
|
111
|
+
return new Response(JSON.stringify({
|
|
112
|
+
message: 'Success',
|
|
113
|
+
count: result.count,
|
|
114
|
+
}), {
|
|
115
|
+
status: 200,
|
|
116
|
+
headers: { 'Content-Type': 'application/json' },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return new Response(JSON.stringify({
|
|
120
|
+
message: 'Error processing webhook',
|
|
121
|
+
error: result.error,
|
|
122
|
+
}), {
|
|
123
|
+
status: 500,
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/webhook/handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A/B Testing Webhook Handlers
|
|
3
|
+
*
|
|
4
|
+
* Utilities for processing Contentful webhooks to sync PageTest configurations.
|
|
5
|
+
*/
|
|
6
|
+
export type { RawPageTest, WebhookHandlerConfig, WebhookResult, } from './handler';
|
|
7
|
+
export { createWebhookHandler, normalizeVariantConfig, processAbTestWebhook, transformPageTest, } from './handler';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACV,WAAW,EACX,oBAAoB,EACpB,aAAa,GACd,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A/B Testing Webhook Handlers
|
|
3
|
+
*
|
|
4
|
+
* Utilities for processing Contentful webhooks to sync PageTest configurations.
|
|
5
|
+
*/
|
|
6
|
+
export { createWebhookHandler, normalizeVariantConfig, processAbTestWebhook, transformPageTest, } from './handler';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,WAAW,CAAC"}
|
package/docs/llms.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @se-studio/ab-testing — LLM Reference
|
|
2
|
+
|
|
3
|
+
Server-side A/B testing framework for Next.js App Router with Contentful CMS. Variants are assigned via cookies; decisions happen in Next.js `proxy` (Next 16) or `middleware` (Next 15). Call `createAbTestMiddleware` / `createStaticAbTestMiddleware` from either file — the factory API is unchanged.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
- **Test**: a named experiment with 2+ variants, each mapped to a URL path
|
|
8
|
+
- **Assignment**: a cookie that records which variant a user is seeing
|
|
9
|
+
- **Middleware**: intercepts requests and rewrites URLs to the assigned variant path
|
|
10
|
+
|
|
11
|
+
## Key Types
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import type {
|
|
15
|
+
AbTest, // { id, name, variants: AbTestVariant[] }
|
|
16
|
+
AbTestVariant, // { id, name, path: string, weight: number }
|
|
17
|
+
AbTestAssignment, // { testId, variantId }
|
|
18
|
+
AbTestCookie, // serialised cookie value
|
|
19
|
+
AbTestVariantConfig, // { variantId, contentPath }
|
|
20
|
+
IBlobStore, // storage interface for test configs (Vercel Blob)
|
|
21
|
+
} from '@se-studio/ab-testing';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Middleware Setup
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// src/proxy.ts (Next 16) or src/middleware.ts (Next 15)
|
|
28
|
+
import { createAbTestMiddleware } from '@se-studio/ab-testing';
|
|
29
|
+
|
|
30
|
+
const abTestMiddleware = createAbTestMiddleware({
|
|
31
|
+
blobStore, // IBlobStore — fetches test configs from Vercel Blob
|
|
32
|
+
cookieName: 'ab_assignment', // optional, default: DEFAULT_COOKIE_NAME
|
|
33
|
+
cookieMaxAge: 60 * 60 * 24 * 30, // optional, default: DEFAULT_COOKIE_MAX_AGE
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export async function proxy(request: NextRequest) {
|
|
37
|
+
return abTestMiddleware(request);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For static (no blob store) tests:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { createStaticAbTestMiddleware } from '@se-studio/ab-testing';
|
|
45
|
+
|
|
46
|
+
const abTestMiddleware = createStaticAbTestMiddleware({
|
|
47
|
+
tests: [
|
|
48
|
+
{
|
|
49
|
+
id: 'hero-test',
|
|
50
|
+
variants: [
|
|
51
|
+
{ id: 'control', path: '/home', weight: 50 },
|
|
52
|
+
{ id: 'variant-a', path: '/home-v2', weight: 50 },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Reading the Assignment (Client / Server)
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { useAbTestAssignments } from '@se-studio/ab-testing';
|
|
63
|
+
|
|
64
|
+
// In a React component:
|
|
65
|
+
const { assignments, isLoading } = useAbTestAssignments({ testIds: ['hero-test'] });
|
|
66
|
+
const variant = assignments['hero-test']; // AbTestAssignment | undefined
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Webhook Handler (Contentful → update test cache)
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createWebhookHandler } from '@se-studio/ab-testing';
|
|
73
|
+
|
|
74
|
+
const handler = createWebhookHandler({
|
|
75
|
+
blobStore,
|
|
76
|
+
secret: process.env.WEBHOOK_SECRET!,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// In a Next.js route handler:
|
|
80
|
+
export const POST = handler.handle;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Cookie Utilities
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import {
|
|
87
|
+
parseCookie,
|
|
88
|
+
serializeCookie,
|
|
89
|
+
readCookieFromDocument, // client-side
|
|
90
|
+
DEFAULT_COOKIE_NAME,
|
|
91
|
+
DEFAULT_COOKIE_MAX_AGE,
|
|
92
|
+
} from '@se-studio/ab-testing';
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Low-Level Utilities
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import {
|
|
99
|
+
selectVariant, // weighted random variant selection
|
|
100
|
+
getAssignment, // read assignment from cookies
|
|
101
|
+
setAssignment, // write assignment to response cookies
|
|
102
|
+
isValidAssignment, // validate an assignment object
|
|
103
|
+
normalizePath, // normalise URL paths for comparison
|
|
104
|
+
findCanonicalPath, // resolve variant path back to canonical
|
|
105
|
+
} from '@se-studio/ab-testing';
|
|
106
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@se-studio/ab-testing",
|
|
3
|
+
"version": "0.0.0-next16-20260822103555",
|
|
4
|
+
"description": "Server-side A/B testing framework for Next.js applications with Contentful CMS",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Something-Else-Studio/se-core-product",
|
|
8
|
+
"directory": "packages/ab-testing"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./middleware": {
|
|
20
|
+
"types": "./dist/middleware/index.d.ts",
|
|
21
|
+
"import": "./dist/middleware/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./webhook": {
|
|
24
|
+
"types": "./dist/webhook/index.d.ts",
|
|
25
|
+
"import": "./dist/webhook/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./hooks": {
|
|
28
|
+
"types": "./dist/hooks/index.d.ts",
|
|
29
|
+
"import": "./dist/hooks/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./codegen": {
|
|
32
|
+
"types": "./dist/codegen/index.d.ts",
|
|
33
|
+
"import": "./dist/codegen/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./components": {
|
|
36
|
+
"types": "./dist/components/index.d.ts",
|
|
37
|
+
"import": "./dist/components/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./reporting": {
|
|
40
|
+
"types": "./dist/reporting/buildExperimentImpression.d.ts",
|
|
41
|
+
"import": "./dist/reporting/buildExperimentImpression.js"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"bin": {
|
|
45
|
+
"generate-ab-tests": "./dist/codegen/cli.js"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"*.md",
|
|
50
|
+
"docs"
|
|
51
|
+
],
|
|
52
|
+
"keywords": [
|
|
53
|
+
"ab-testing",
|
|
54
|
+
"a/b-testing",
|
|
55
|
+
"experiment",
|
|
56
|
+
"nextjs",
|
|
57
|
+
"next.js",
|
|
58
|
+
"middleware",
|
|
59
|
+
"contentful",
|
|
60
|
+
"cms",
|
|
61
|
+
"typescript",
|
|
62
|
+
"react",
|
|
63
|
+
"analytics"
|
|
64
|
+
],
|
|
65
|
+
"homepage": "https://github.com/Something-Else-Studio/se-core-product#readme",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/Something-Else-Studio/se-core-product/issues"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"next": ">=15.5.0 <17",
|
|
71
|
+
"react": "^19.0.0",
|
|
72
|
+
"@se-studio/core-ui": "0.0.0-next16-20260822103555"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@biomejs/biome": "^2.5.5",
|
|
76
|
+
"@types/node": "^24.13.3",
|
|
77
|
+
"@types/react": "^19.2.17",
|
|
78
|
+
"next": "^16.3.1",
|
|
79
|
+
"typescript": "^6.0.3",
|
|
80
|
+
"vitest": "^4.1.10",
|
|
81
|
+
"@se-studio/core-ui": "0.0.0-next16-20260822103555"
|
|
82
|
+
},
|
|
83
|
+
"scripts": {
|
|
84
|
+
"build": "tsc --project tsconfig.build.json",
|
|
85
|
+
"dev": "tsc --project tsconfig.build.json --watch",
|
|
86
|
+
"type-check": "tsc --noEmit",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"lint": "biome lint .",
|
|
89
|
+
"clean": "rm -rf dist .turbo *.tsbuildinfo"
|
|
90
|
+
}
|
|
91
|
+
}
|