@varykit/nuxt 0.1.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 +91 -0
- package/dist/module.d.mts +11 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +75 -0
- package/dist/runtime/VaryShow.d.vue.ts +18 -0
- package/dist/runtime/VaryShow.vue +17 -0
- package/dist/runtime/VaryShow.vue.d.ts +18 -0
- package/dist/runtime/useSegment.d.ts +17 -0
- package/dist/runtime/useSegment.js +13 -0
- package/dist/runtime/vary-resolver.d.vue.ts +2 -0
- package/dist/runtime/vary-resolver.vue +35 -0
- package/dist/runtime/vary-resolver.vue.d.ts +2 -0
- package/dist/types.d.mts +3 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @varykit/nuxt
|
|
2
|
+
|
|
3
|
+
**Nuxt 4 module for VaryKit: file-based page testing plus SSR-safe element-level targeting.**
|
|
4
|
+
|
|
5
|
+
This module depends on `@varykit/vue` and `@varykit/core` as real dependencies — you never need to install them separately.
|
|
6
|
+
|
|
7
|
+
## The segment model
|
|
8
|
+
|
|
9
|
+
Every visitor is assigned to a **single global segment** (e.g. `control` or `treatment`), persisted in the `vary:segment` cookie. That segment is reused consistently across every page and element — a visitor is never in different segments for different tests.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @varykit/nuxt
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Add `@varykit/nuxt` to the `modules` array of your `nuxt.config.ts`:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
export default defineNuxtConfig({
|
|
23
|
+
modules: ['@varykit/nuxt'],
|
|
24
|
+
|
|
25
|
+
varyOptions: {
|
|
26
|
+
duration: 60 * 60 * 24 * 30, // global default: 30 days
|
|
27
|
+
|
|
28
|
+
// Global segments — every visitor is assigned to exactly one. The segment
|
|
29
|
+
// name is the variant file suffix: "a" serves `__a.vue`, "b" serves
|
|
30
|
+
// `__b.vue`, etc. Files with no matching segment are never served.
|
|
31
|
+
segments: {
|
|
32
|
+
a: { distribution: 0.2 },
|
|
33
|
+
b: { distribution: 0.5 }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## File-based page testing
|
|
40
|
+
|
|
41
|
+
Create variant files using the `__variant` naming convention (flat or folder-based):
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
pages/
|
|
45
|
+
contact.__a.vue
|
|
46
|
+
contact.__b.vue
|
|
47
|
+
contact.__c.vue
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
All variants collapse into one route (`/contact`). The visitor's global segment name is used directly as the variant suffix, so with the config above (`segments: { a, b }`) only `contact.__a.vue` and `contact.__b.vue` are served — `contact.__c.vue` is never shown because there is no `c` segment.
|
|
51
|
+
|
|
52
|
+
## Element-level targeting
|
|
53
|
+
|
|
54
|
+
`@varykit/nuxt` provides `useSegment` and `<VaryShow>` (auto-imported) with the cookie mechanism swapped to Nuxt's SSR-safe `useCookie` so element targeting is also SSR-consistent (no flash of the wrong variant).
|
|
55
|
+
|
|
56
|
+
Because segments are already defined in `varyOptions.segments`, `<VaryShow>` picks them up from runtime config automatically — you only pass the `segment` to match:
|
|
57
|
+
|
|
58
|
+
```vue
|
|
59
|
+
<template>
|
|
60
|
+
<VaryShow segment="a">
|
|
61
|
+
<p>A content</p>
|
|
62
|
+
</VaryShow>
|
|
63
|
+
|
|
64
|
+
<VaryShow segment="b">
|
|
65
|
+
<p>B content</p>
|
|
66
|
+
</VaryShow>
|
|
67
|
+
</template>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
You can still override segments per-use by passing the `:segments` prop explicitly.
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
### `duration`
|
|
75
|
+
|
|
76
|
+
Global default cookie duration in seconds. Defaults to 30 days.
|
|
77
|
+
|
|
78
|
+
### `segments`
|
|
79
|
+
|
|
80
|
+
The global segment definitions. Each segment supports:
|
|
81
|
+
|
|
82
|
+
| Option | Type | Description |
|
|
83
|
+
| ------ | ---- | ----------- |
|
|
84
|
+
| `distribution` | `number` | Weight between `0` and `1`. Omitted segments auto-split the remainder. |
|
|
85
|
+
| `duration` | `number` | Cookie duration in seconds, overrides the global default for this segment. |
|
|
86
|
+
|
|
87
|
+
The segment name is the variant file suffix used everywhere — pages and elements simply follow the visitor's segment.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { NuxtModule } from '@nuxt/schema';
|
|
2
|
+
import { VarySegments } from '@varykit/core';
|
|
3
|
+
|
|
4
|
+
interface ModuleOptions {
|
|
5
|
+
duration?: number;
|
|
6
|
+
segments?: VarySegments;
|
|
7
|
+
}
|
|
8
|
+
declare const module$1: NuxtModule<ModuleOptions>;
|
|
9
|
+
|
|
10
|
+
export { module$1 as default };
|
|
11
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addImports, addComponent } from '@nuxt/kit';
|
|
2
|
+
import { validateSegments } from '@varykit/core';
|
|
3
|
+
|
|
4
|
+
const FLAT_REGEX = /\.__([a-zA-Z0-9_-]+)$/;
|
|
5
|
+
const FOLDER_REGEX = /\/__([a-zA-Z0-9_-]+)$/;
|
|
6
|
+
const module$1 = defineNuxtModule({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "@varykit/nuxt",
|
|
9
|
+
configKey: "varyOptions"
|
|
10
|
+
},
|
|
11
|
+
defaults: {
|
|
12
|
+
duration: 60 * 60 * 24 * 30,
|
|
13
|
+
// 30 days
|
|
14
|
+
segments: {}
|
|
15
|
+
},
|
|
16
|
+
setup(options, nuxt) {
|
|
17
|
+
const { resolve } = createResolver(import.meta.url);
|
|
18
|
+
validateSegments(options.segments ?? {});
|
|
19
|
+
addImports({
|
|
20
|
+
name: "useSegment",
|
|
21
|
+
as: "useSegment",
|
|
22
|
+
from: resolve("./runtime/useSegment")
|
|
23
|
+
});
|
|
24
|
+
addComponent({
|
|
25
|
+
name: "VaryShow",
|
|
26
|
+
filePath: resolve("./runtime/VaryShow.vue")
|
|
27
|
+
});
|
|
28
|
+
nuxt.hook("pages:extend", (pages) => {
|
|
29
|
+
const groups = {};
|
|
30
|
+
for (const page of pages) {
|
|
31
|
+
const name = page.name || "";
|
|
32
|
+
const flatMatch = name.match(FLAT_REGEX);
|
|
33
|
+
const folderMatch = name.match(FOLDER_REGEX);
|
|
34
|
+
const match = flatMatch || folderMatch;
|
|
35
|
+
if (!match) continue;
|
|
36
|
+
const base = name.slice(0, match.index);
|
|
37
|
+
const variant = match[1];
|
|
38
|
+
groups[base] ??= [];
|
|
39
|
+
groups[base].push({ page, variant });
|
|
40
|
+
}
|
|
41
|
+
for (const [base, entries] of Object.entries(groups)) {
|
|
42
|
+
if (entries.length < 2) continue;
|
|
43
|
+
for (const { page } of entries) {
|
|
44
|
+
const idx = pages.indexOf(page);
|
|
45
|
+
if (idx !== -1) pages.splice(idx, 1);
|
|
46
|
+
}
|
|
47
|
+
const rawPath = entries[0].page.path;
|
|
48
|
+
const cleanPath = rawPath.replace(/\.__[a-zA-Z0-9_-]+$/, "").replace(/\/__[a-zA-Z0-9_-]+$/, "") || "/";
|
|
49
|
+
const configKey = cleanPath.replace(/^\//, "");
|
|
50
|
+
const plainIndex = pages.findIndex(
|
|
51
|
+
(p) => (p.name === base || p.path === cleanPath) && !p.file.includes("vary-resolver")
|
|
52
|
+
);
|
|
53
|
+
const hasFallback = plainIndex !== -1;
|
|
54
|
+
if (plainIndex !== -1) pages.splice(plainIndex, 1);
|
|
55
|
+
const variantNames = entries.map((e) => e.variant);
|
|
56
|
+
pages.push({
|
|
57
|
+
name: base || "index",
|
|
58
|
+
path: cleanPath,
|
|
59
|
+
file: resolve("./runtime/vary-resolver.vue"),
|
|
60
|
+
meta: {
|
|
61
|
+
varyBase: configKey,
|
|
62
|
+
varyVariants: variantNames,
|
|
63
|
+
varyFallback: hasFallback
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
nuxt.options.runtimeConfig.public.varyOptions = {
|
|
69
|
+
duration: options.duration,
|
|
70
|
+
segments: options.segments
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { VarySegments } from '@varykit/core';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
segment: string;
|
|
4
|
+
segments?: VarySegments;
|
|
5
|
+
duration?: number;
|
|
6
|
+
};
|
|
7
|
+
declare var __VLS_1: {};
|
|
8
|
+
type __VLS_Slots = {} & {
|
|
9
|
+
default?: (props: typeof __VLS_1) => any;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
13
|
+
export default _default;
|
|
14
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
15
|
+
new (): {
|
|
16
|
+
$slots: S;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useSegment } from "./useSegment";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
segment: { type: String, required: true },
|
|
5
|
+
segments: { type: Object, required: false },
|
|
6
|
+
duration: { type: Number, required: false }
|
|
7
|
+
});
|
|
8
|
+
const runtimeConfig = useRuntimeConfig().public.varyOptions;
|
|
9
|
+
const segments = props.segments ?? runtimeConfig.segments ?? {};
|
|
10
|
+
const { segment } = useSegment(segments, {
|
|
11
|
+
duration: props.duration
|
|
12
|
+
});
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<slot v-if="segment === props.segment" />
|
|
17
|
+
</template>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { VarySegments } from '@varykit/core';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
segment: string;
|
|
4
|
+
segments?: VarySegments;
|
|
5
|
+
duration?: number;
|
|
6
|
+
};
|
|
7
|
+
declare var __VLS_1: {};
|
|
8
|
+
type __VLS_Slots = {} & {
|
|
9
|
+
default?: (props: typeof __VLS_1) => any;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
12
|
+
declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
|
|
13
|
+
export default _default;
|
|
14
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
15
|
+
new (): {
|
|
16
|
+
$slots: S;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Ref } from 'vue';
|
|
2
|
+
import { type VarySegments } from '@varykit/core';
|
|
3
|
+
export interface UseSegmentOptions {
|
|
4
|
+
duration?: number;
|
|
5
|
+
cookieName?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UseSegmentReturn {
|
|
8
|
+
segment: Ref<string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Nuxt-specific global segment resolution.
|
|
12
|
+
*
|
|
13
|
+
* Identical API to `@varykit/vue`'s `useSegment`, but persists the assignment
|
|
14
|
+
* via Nuxt's SSR-safe `useCookie` so the resolved segment is consistent
|
|
15
|
+
* between server and client (no flash of the wrong variant).
|
|
16
|
+
*/
|
|
17
|
+
export declare function useSegment(segments: VarySegments, options?: UseSegmentOptions): UseSegmentReturn;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { resolveSegment } from "@varykit/core";
|
|
3
|
+
export function useSegment(segments, options = {}) {
|
|
4
|
+
const cookieName = options.cookieName ?? "vary:segment";
|
|
5
|
+
const cookie = useCookie(cookieName);
|
|
6
|
+
const resolved = resolveSegment({
|
|
7
|
+
segments,
|
|
8
|
+
existingSegment: cookie.value,
|
|
9
|
+
defaultDuration: options.duration
|
|
10
|
+
});
|
|
11
|
+
useCookie(cookieName, { maxAge: resolved.duration }).value = resolved.segment;
|
|
12
|
+
return { segment: ref(resolved.segment) };
|
|
13
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { resolveSegment } from "@varykit/core";
|
|
3
|
+
const route = useRoute();
|
|
4
|
+
const runtimeConfig = useRuntimeConfig().public.varyOptions;
|
|
5
|
+
const base = route.meta.varyBase;
|
|
6
|
+
const discoveredVariants = route.meta.varyVariants;
|
|
7
|
+
const hasFallback = route.meta.varyFallback === true;
|
|
8
|
+
const globalDuration = runtimeConfig.duration;
|
|
9
|
+
const segments = runtimeConfig.segments || {};
|
|
10
|
+
const cookieName = "vary:segment";
|
|
11
|
+
const cookie = useCookie(cookieName);
|
|
12
|
+
const resolved = resolveSegment({
|
|
13
|
+
segments,
|
|
14
|
+
existingSegment: cookie.value,
|
|
15
|
+
defaultDuration: globalDuration
|
|
16
|
+
});
|
|
17
|
+
useCookie(cookieName, { maxAge: resolved.duration }).value = resolved.segment;
|
|
18
|
+
const variant = resolved.segment;
|
|
19
|
+
const modules = import.meta.glob("~/pages/**/*.vue");
|
|
20
|
+
const variantKey = Object.keys(modules).find(
|
|
21
|
+
(k) => k.endsWith(`.__${variant}.vue`) || k.endsWith(`/__${variant}.vue`)
|
|
22
|
+
);
|
|
23
|
+
const fallbackKey = hasFallback ? Object.keys(modules).find(
|
|
24
|
+
(k) => k.endsWith(`/${base}.vue`) && !/\.__[a-zA-Z0-9_-]+\.vue$/.test(k)
|
|
25
|
+
) : void 0;
|
|
26
|
+
const key = variantKey ?? fallbackKey ?? discoveredVariants[0];
|
|
27
|
+
if (!key) {
|
|
28
|
+
throw new Error(`[varykit] No variant file for segment "${variant}" of "${base}".`);
|
|
29
|
+
}
|
|
30
|
+
const VariantComponent = defineAsyncComponent(() => modules[key]());
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<VariantComponent />
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
2
|
+
export default _default;
|
package/dist/types.d.mts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varykit/nuxt",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Nuxt 4 module for VaryKit: file-based page testing plus SSR-safe element-level targeting.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/types.d.mts",
|
|
10
|
+
"import": "./dist/module.mjs"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/module.mjs",
|
|
14
|
+
"types": "./dist/types.d.mts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "nuxt-module-build",
|
|
20
|
+
"dev:prepare": "nuxt-module-build --stub",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"nuxt",
|
|
25
|
+
"nuxt-module",
|
|
26
|
+
"a/b-testing",
|
|
27
|
+
"multivariate",
|
|
28
|
+
"ab-test",
|
|
29
|
+
"experimentation",
|
|
30
|
+
"bucketing",
|
|
31
|
+
"varykit"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@nuxt/kit": "^4.0.0",
|
|
35
|
+
"@varykit/core": "workspace:*",
|
|
36
|
+
"@varykit/vue": "workspace:*"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@nuxt/module-builder": "1.0.2",
|
|
40
|
+
"@nuxt/schema": "^4.0.0",
|
|
41
|
+
"nuxt": "^4.0.0",
|
|
42
|
+
"typescript": "^5.0.0",
|
|
43
|
+
"vue": "^3.0.0",
|
|
44
|
+
"vue-tsc": "^2.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"nuxt": "^4.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|