@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 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 };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@varykit/nuxt",
3
+ "configKey": "varyOptions",
4
+ "version": "0.1.0",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -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;
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
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
+ }