@replohq/sdk 2.16.0 → 2.18.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/_vendor/replo-utils/lib/pageRouteMatch.d.ts +10 -0
- package/_vendor/replo-utils/lib/sitePages.d.ts +28 -0
- package/_vendor/schemas/placeholderProductsProtocol.d.ts +1 -0
- package/_vendor/schemas/placeholderProductsProtocol.mjs +2 -1
- package/_vendor/schemas/routing/experiments.d.ts +22 -1
- package/_vendor/schemas/routing/experiments.mjs +235 -6
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
- package/routing/experiments.js +26 -14
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function normalizePageRoute(route: string): string;
|
|
2
|
+
export declare function getPageForRoute<PageType extends {
|
|
3
|
+
route: string;
|
|
4
|
+
}>(pages: PageType[], route: string): PageType | undefined;
|
|
5
|
+
/** The values a route fills a template with (`/r/[city]` + `/r/oakland` → `{ city: "oakland" }`), or null when it doesn't fill it; a catch-all value keeps its slashes. */
|
|
6
|
+
export declare function matchPageRouteParams(template: string, route: string): Record<string, string> | null;
|
|
7
|
+
/** The route a template renders for `params`; null when one is missing. An empty optional catch-all drops its segment. */
|
|
8
|
+
export declare function fillPageRoute(template: string, params: Readonly<Record<string, string>>): string | null;
|
|
9
|
+
/** A template's params in order (`[state]/[city]`), so two templates with the same shape render the same set of pages. */
|
|
10
|
+
export declare function pageRouteParamShape(route: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare function isDynamicRouteSegment(segment: string): boolean;
|
|
2
|
+
/** Whether `route` is a template that serves many URLs, such as `/products/[slug]`. */
|
|
3
|
+
export declare function isDynamicRoute(route: string): boolean;
|
|
4
|
+
export declare const SITE_PAGE_FILE_NAMES: {
|
|
5
|
+
readonly legacy: "page.tsx";
|
|
6
|
+
readonly draft: "page.dev.tsx";
|
|
7
|
+
readonly live: "page.published.tsx";
|
|
8
|
+
};
|
|
9
|
+
export type SitePageFileKind = keyof typeof SITE_PAGE_FILE_NAMES;
|
|
10
|
+
/** The project-relative page file a URL path like `/about` installs to; null for a path that can't name one. */
|
|
11
|
+
export declare function getSitePageFilePath(route: string): string | null;
|
|
12
|
+
export declare const SITE_PAGE_PUBLISHED_FILE_EXTENSION = ".published.tsx";
|
|
13
|
+
export declare const SITE_PAGE_FILE_GLOBS: string[];
|
|
14
|
+
export declare const SITE_PAGE_PUBLISH_STATES: readonly ["published", "publishedWithChanges", "unpublished", "pendingDelete", "legacy"];
|
|
15
|
+
export type SitePagePublishState = (typeof SITE_PAGE_PUBLISH_STATES)[number];
|
|
16
|
+
export declare function getSitePageFileKind(filepath: string): SitePageFileKind | null;
|
|
17
|
+
export declare function isSupportedSitePageFilepath(filepath: string): boolean;
|
|
18
|
+
export declare function deriveSitePagePublishState({ kinds, draftMatchesLive, }: {
|
|
19
|
+
kinds: ReadonlySet<SitePageFileKind>;
|
|
20
|
+
draftMatchesLive: boolean | null;
|
|
21
|
+
}): SitePagePublishState | "invalid";
|
|
22
|
+
export declare function groupSitePageFiles<PageFileType extends {
|
|
23
|
+
filepath: string;
|
|
24
|
+
route: string;
|
|
25
|
+
contentHash?: string;
|
|
26
|
+
}>(files: PageFileType[]): (PageFileType & {
|
|
27
|
+
publishState: SitePagePublishState;
|
|
28
|
+
})[];
|
|
@@ -2,6 +2,7 @@ export declare const PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID: "replo:placehol
|
|
|
2
2
|
export declare const PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION: 3;
|
|
3
3
|
export declare const PLACEHOLDER_HERO_PRODUCT_ID = "f0921037-e530-4e67-a612-d573b7c40b19";
|
|
4
4
|
export declare const PLACEHOLDER_PRODUCT_ID = "c53f98cd-0989-4661-a7c2-a0304d184676";
|
|
5
|
+
export declare const PLACEHOLDER_PRODUCT_LIMIT = 12;
|
|
5
6
|
export type PlaceholderProductId = typeof PLACEHOLDER_HERO_PRODUCT_ID | typeof PLACEHOLDER_PRODUCT_ID;
|
|
6
7
|
export declare const PLACEHOLDER_PRODUCTS_SOURCE_KEY = "__REPLO_PLACEHOLDER_PRODUCTS__";
|
|
7
8
|
export declare const PLACEHOLDER_PRODUCTS_CHANGE_EVENT = "replo:placeholder-products";
|
|
@@ -2,7 +2,8 @@ var PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID = "replo:placeholder-products-ifr
|
|
|
2
2
|
var PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION = 3;
|
|
3
3
|
var PLACEHOLDER_HERO_PRODUCT_ID = "f0921037-e530-4e67-a612-d573b7c40b19";
|
|
4
4
|
var PLACEHOLDER_PRODUCT_ID = "c53f98cd-0989-4661-a7c2-a0304d184676";
|
|
5
|
+
var PLACEHOLDER_PRODUCT_LIMIT = 12;
|
|
5
6
|
var PLACEHOLDER_PRODUCTS_SOURCE_KEY = "__REPLO_PLACEHOLDER_PRODUCTS__";
|
|
6
7
|
var PLACEHOLDER_PRODUCTS_CHANGE_EVENT = "replo:placeholder-products";
|
|
7
8
|
|
|
8
|
-
export { PLACEHOLDER_HERO_PRODUCT_ID, PLACEHOLDER_PRODUCTS_CHANGE_EVENT, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION, PLACEHOLDER_PRODUCTS_SOURCE_KEY, PLACEHOLDER_PRODUCT_ID };
|
|
9
|
+
export { PLACEHOLDER_HERO_PRODUCT_ID, PLACEHOLDER_PRODUCTS_CHANGE_EVENT, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_ID, PLACEHOLDER_PRODUCTS_IFRAME_RPC_CHANNEL_VERSION, PLACEHOLDER_PRODUCTS_SOURCE_KEY, PLACEHOLDER_PRODUCT_ID, PLACEHOLDER_PRODUCT_LIMIT };
|
|
@@ -15,6 +15,7 @@ export declare const experimentStatusSchema: z.ZodEnum<{
|
|
|
15
15
|
export declare const experimentSchema: z.ZodObject<{
|
|
16
16
|
experimentKey: z.ZodString;
|
|
17
17
|
route: z.ZodString;
|
|
18
|
+
includedRoutes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
18
19
|
status: z.ZodEnum<{
|
|
19
20
|
running: "running";
|
|
20
21
|
draft: "draft";
|
|
@@ -31,6 +32,7 @@ export declare const experimentsFileSchema: z.ZodObject<{
|
|
|
31
32
|
experiments: z.ZodArray<z.ZodObject<{
|
|
32
33
|
experimentKey: z.ZodString;
|
|
33
34
|
route: z.ZodString;
|
|
35
|
+
includedRoutes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
34
36
|
status: z.ZodEnum<{
|
|
35
37
|
running: "running";
|
|
36
38
|
draft: "draft";
|
|
@@ -59,10 +61,29 @@ export declare function assignVariant({ visitorId, experiment, }: {
|
|
|
59
61
|
experiment: Pick<Experiment, "experimentKey" | "variants">;
|
|
60
62
|
}): ExperimentVariant;
|
|
61
63
|
/** The experiment that routes `pathname`: a running one, else one that shipped a non-control variant. */
|
|
62
|
-
export declare function findActiveExperiment({ experiments, pathname, }: {
|
|
64
|
+
export declare function findActiveExperiment({ experiments, pathname, liveRoutes, }: {
|
|
63
65
|
experiments: readonly Experiment[];
|
|
64
66
|
pathname: string;
|
|
67
|
+
/** The build's page routes; a page of its own beats a template that would also fill `pathname`, as in Next.js. */
|
|
68
|
+
liveRoutes: readonly string[] | undefined;
|
|
65
69
|
}): Experiment | null;
|
|
70
|
+
/** Whether a visit to `pathname` is in the experiment: its route, or a page its template renders and the test includes. */
|
|
71
|
+
export declare function experimentCoversPath({ experiment, pathname, liveRoutes, }: {
|
|
72
|
+
experiment: Pick<Experiment, "route" | "includedRoutes">;
|
|
73
|
+
pathname: string;
|
|
74
|
+
liveRoutes: readonly string[] | undefined;
|
|
75
|
+
}): boolean;
|
|
76
|
+
/** The path to render for a visit to `pathname` in `variant`: its route, with a template's params filled from the visit. */
|
|
77
|
+
export declare function variantPathFor({ experiment, variant, pathname, }: {
|
|
78
|
+
experiment: Pick<Experiment, "route">;
|
|
79
|
+
variant: ExperimentVariant;
|
|
80
|
+
pathname: string;
|
|
81
|
+
}): string | null;
|
|
82
|
+
/** Whether `pathname` is one of a variant's pages: its route, or a page its template renders. */
|
|
83
|
+
export declare function isVariantPath({ variant, pathname, }: {
|
|
84
|
+
variant: ExperimentVariant;
|
|
85
|
+
pathname: string;
|
|
86
|
+
}): boolean;
|
|
66
87
|
/** Whether a stopped experiment still routes every visitor to its shipped variant. */
|
|
67
88
|
export declare function hasNonControlShippedVariant(experiment: Experiment): boolean;
|
|
68
89
|
/** Whether the experiment routes traffic, and so belongs in the site's routing. */
|
|
@@ -1,6 +1,141 @@
|
|
|
1
1
|
import { EXPERIMENT_KEY_PATTERN } from '../../../chunk-PH6IR743.mjs';
|
|
2
2
|
import * as z from 'zod';
|
|
3
3
|
|
|
4
|
+
var DYNAMIC_ROUTE_SEGMENT_PATTERN = /^\[{1,2}(?:\.\.\.)?[^\]]+\]{1,2}$/u;
|
|
5
|
+
function isDynamicRouteSegment(segment) {
|
|
6
|
+
return DYNAMIC_ROUTE_SEGMENT_PATTERN.test(segment);
|
|
7
|
+
}
|
|
8
|
+
function isDynamicRoute(route) {
|
|
9
|
+
return route.split("/").some(isDynamicRouteSegment);
|
|
10
|
+
}
|
|
11
|
+
var SITE_PAGE_FILE_NAMES = {
|
|
12
|
+
legacy: "page.tsx",
|
|
13
|
+
draft: "page.dev.tsx",
|
|
14
|
+
live: "page.published.tsx"
|
|
15
|
+
};
|
|
16
|
+
Object.values(SITE_PAGE_FILE_NAMES).map(
|
|
17
|
+
(name) => `app/**/${name}`
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
function normalizePageRoute(route) {
|
|
21
|
+
const trimmedRoute = route.trim().replace(/^\/+|\/+$/gu, "");
|
|
22
|
+
return trimmedRoute ? `/${trimmedRoute}` : "/";
|
|
23
|
+
}
|
|
24
|
+
function routeSegments(route) {
|
|
25
|
+
return route.split("/").filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
function isCatchAllSegment(segment) {
|
|
28
|
+
return segment.startsWith("[...");
|
|
29
|
+
}
|
|
30
|
+
function isOptionalCatchAllSegment(segment) {
|
|
31
|
+
return segment.startsWith("[[...");
|
|
32
|
+
}
|
|
33
|
+
function routeMatchesDynamicRoute(dynamicRoute, concreteRoute) {
|
|
34
|
+
const dynamicSegments = routeSegments(dynamicRoute);
|
|
35
|
+
const concreteSegments = routeSegments(concreteRoute);
|
|
36
|
+
for (const [index, dynamicSegment] of dynamicSegments.entries()) {
|
|
37
|
+
if (isCatchAllSegment(dynamicSegment) || isOptionalCatchAllSegment(dynamicSegment)) {
|
|
38
|
+
return index === dynamicSegments.length - 1 && concreteSegments.length >= index + (isCatchAllSegment(dynamicSegment) ? 1 : 0);
|
|
39
|
+
}
|
|
40
|
+
const concreteSegment = concreteSegments[index];
|
|
41
|
+
if (!concreteSegment) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (!isDynamicRouteSegment(dynamicSegment) && dynamicSegment !== concreteSegment) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return dynamicSegments.length === concreteSegments.length;
|
|
49
|
+
}
|
|
50
|
+
function segmentSpecificity(segment) {
|
|
51
|
+
if (isOptionalCatchAllSegment(segment)) {
|
|
52
|
+
return 3;
|
|
53
|
+
}
|
|
54
|
+
if (isCatchAllSegment(segment)) {
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
return isDynamicRouteSegment(segment) ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
function compareTemplateSpecificity(leftRoute, rightRoute) {
|
|
60
|
+
const leftSegments = routeSegments(leftRoute);
|
|
61
|
+
const rightSegments = routeSegments(rightRoute);
|
|
62
|
+
const length = Math.max(leftSegments.length, rightSegments.length);
|
|
63
|
+
for (let index = 0; index < length; index += 1) {
|
|
64
|
+
const difference = segmentSpecificity(leftSegments[index] ?? "") - segmentSpecificity(rightSegments[index] ?? "");
|
|
65
|
+
if (difference !== 0) {
|
|
66
|
+
return difference;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
function getPageForRoute(pages, route) {
|
|
72
|
+
const normalizedRoute = normalizePageRoute(route);
|
|
73
|
+
const exactMatch = pages.find((page) => {
|
|
74
|
+
return normalizePageRoute(page.route) === normalizedRoute;
|
|
75
|
+
});
|
|
76
|
+
if (exactMatch) {
|
|
77
|
+
return exactMatch;
|
|
78
|
+
}
|
|
79
|
+
const templates = pages.filter((page) => {
|
|
80
|
+
const candidateRoute = normalizePageRoute(page.route);
|
|
81
|
+
return isDynamicRoute(candidateRoute) && routeMatchesDynamicRoute(candidateRoute, normalizedRoute);
|
|
82
|
+
});
|
|
83
|
+
return templates.sort((leftPage, rightPage) => {
|
|
84
|
+
return compareTemplateSpecificity(
|
|
85
|
+
normalizePageRoute(leftPage.route),
|
|
86
|
+
normalizePageRoute(rightPage.route)
|
|
87
|
+
);
|
|
88
|
+
})[0];
|
|
89
|
+
}
|
|
90
|
+
var PARAM_NAME_PATTERN = /^\[{1,2}(?:\.\.\.)?([^\]]+?)\]{1,2}$/u;
|
|
91
|
+
function matchPageRouteParams(template, route) {
|
|
92
|
+
const templateSegments = routeSegments(template);
|
|
93
|
+
const segments = routeSegments(route);
|
|
94
|
+
const params = {};
|
|
95
|
+
for (const [index, templateSegment] of templateSegments.entries()) {
|
|
96
|
+
const name = PARAM_NAME_PATTERN.exec(templateSegment)?.[1];
|
|
97
|
+
if (name && (isCatchAllSegment(templateSegment) || isOptionalCatchAllSegment(templateSegment))) {
|
|
98
|
+
const rest = segments.slice(index);
|
|
99
|
+
const isLast = index === templateSegments.length - 1;
|
|
100
|
+
if (!isLast || rest.length === 0 && isCatchAllSegment(templateSegment)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
params[name] = rest.join("/");
|
|
104
|
+
return params;
|
|
105
|
+
}
|
|
106
|
+
const segment = segments[index];
|
|
107
|
+
if (segment === void 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (name) {
|
|
111
|
+
params[name] = segment;
|
|
112
|
+
} else if (templateSegment !== segment) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return templateSegments.length === segments.length ? params : null;
|
|
117
|
+
}
|
|
118
|
+
function fillPageRoute(template, params) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
for (const segment of routeSegments(template)) {
|
|
121
|
+
const name = PARAM_NAME_PATTERN.exec(segment)?.[1];
|
|
122
|
+
if (!name) {
|
|
123
|
+
parts.push(segment);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const value = params[name];
|
|
127
|
+
if (value === void 0 || value === "" && !isOptionalCatchAllSegment(segment)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
if (value !== "") {
|
|
131
|
+
parts.push(value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return `/${parts.join("/")}`;
|
|
135
|
+
}
|
|
136
|
+
function pageRouteParamShape(route) {
|
|
137
|
+
return routeSegments(route).filter(isDynamicRouteSegment).join("/");
|
|
138
|
+
}
|
|
4
139
|
var MIN_VARIANTS = 2;
|
|
5
140
|
var MAX_VARIANTS = 5;
|
|
6
141
|
var TOTAL_ROLLOUT_PERCENTAGE = 100;
|
|
@@ -22,12 +157,17 @@ var experimentSchema = z.object({
|
|
|
22
157
|
"The experiment's key; changing it reassigns every visitor"
|
|
23
158
|
),
|
|
24
159
|
route: experimentRouteSchema.describe(
|
|
25
|
-
"The shared URL visitors land on; the control variant renders it"
|
|
160
|
+
"The shared URL visitors land on, or a template route like /restaurants/[city] for every page it renders; the control variant renders it"
|
|
161
|
+
),
|
|
162
|
+
includedRoutes: z.array(experimentRouteSchema).default([]).describe(
|
|
163
|
+
"For a template route, the pages it renders that are in the test, e.g. /restaurants/oakland; empty includes all of them"
|
|
26
164
|
),
|
|
27
165
|
status: experimentStatusSchema.describe(
|
|
28
166
|
"Only running experiments, and stopped ones with a shipped variant, route traffic"
|
|
29
167
|
),
|
|
30
|
-
variants: z.array(experimentVariantSchema).min(MIN_VARIANTS).max(MAX_VARIANTS).describe(
|
|
168
|
+
variants: z.array(experimentVariantSchema).min(MIN_VARIANTS).max(MAX_VARIANTS).describe(
|
|
169
|
+
"The control first, then up to four challengers; on a template route each challenger is a template with the same params"
|
|
170
|
+
),
|
|
31
171
|
shippedVariantKey: experimentKeySchema.nullable().describe(
|
|
32
172
|
"Variant key every visitor gets once the experiment has stopped; null keeps the control"
|
|
33
173
|
)
|
|
@@ -40,6 +180,7 @@ var experimentSchema = z.object({
|
|
|
40
180
|
message: "the first variant is the control and must use the experiment's route"
|
|
41
181
|
});
|
|
42
182
|
}
|
|
183
|
+
addTemplateIssues({ experiment, context });
|
|
43
184
|
const totalRolloutPercentage = experiment.variants.reduce(
|
|
44
185
|
(sum, variant) => sum + variant.rolloutPercentage,
|
|
45
186
|
0
|
|
@@ -113,14 +254,59 @@ function assignVariant({
|
|
|
113
254
|
}
|
|
114
255
|
function findActiveExperiment({
|
|
115
256
|
experiments,
|
|
116
|
-
pathname
|
|
257
|
+
pathname,
|
|
258
|
+
liveRoutes
|
|
117
259
|
}) {
|
|
118
|
-
const route = normalizeRoute(pathname);
|
|
119
260
|
const onRoute = experiments.filter((experiment) => {
|
|
120
|
-
return
|
|
261
|
+
return experimentCoversPath({ experiment, pathname, liveRoutes });
|
|
121
262
|
});
|
|
122
263
|
return onRoute.find((experiment) => experiment.status === "running") ?? onRoute.find(hasNonControlShippedVariant) ?? null;
|
|
123
264
|
}
|
|
265
|
+
function experimentCoversPath({
|
|
266
|
+
experiment,
|
|
267
|
+
pathname,
|
|
268
|
+
liveRoutes
|
|
269
|
+
}) {
|
|
270
|
+
const path = normalizeRoute(pathname);
|
|
271
|
+
if (!isDynamicRoute(experiment.route)) {
|
|
272
|
+
return normalizeRoute(experiment.route) === path;
|
|
273
|
+
}
|
|
274
|
+
if (!matchPageRouteParams(experiment.route, path)) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
if (experiment.includedRoutes.length > 0 && !experiment.includedRoutes.some((route) => normalizeRoute(route) === path)) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
if (!liveRoutes) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
const owner = getPageForRoute(
|
|
284
|
+
liveRoutes.map((route) => ({ route })),
|
|
285
|
+
path
|
|
286
|
+
);
|
|
287
|
+
return owner !== void 0 && normalizeRoute(owner.route) === normalizeRoute(experiment.route);
|
|
288
|
+
}
|
|
289
|
+
function variantPathFor({
|
|
290
|
+
experiment,
|
|
291
|
+
variant,
|
|
292
|
+
pathname
|
|
293
|
+
}) {
|
|
294
|
+
if (!isDynamicRoute(variant.route)) {
|
|
295
|
+
return variant.route;
|
|
296
|
+
}
|
|
297
|
+
const params = matchPageRouteParams(
|
|
298
|
+
experiment.route,
|
|
299
|
+
normalizeRoute(pathname)
|
|
300
|
+
);
|
|
301
|
+
return params ? fillPageRoute(variant.route, params) : null;
|
|
302
|
+
}
|
|
303
|
+
function isVariantPath({
|
|
304
|
+
variant,
|
|
305
|
+
pathname
|
|
306
|
+
}) {
|
|
307
|
+
const path = normalizeRoute(pathname);
|
|
308
|
+
return isDynamicRoute(variant.route) ? matchPageRouteParams(variant.route, path) !== null : normalizeRoute(variant.route) === path;
|
|
309
|
+
}
|
|
124
310
|
function hasNonControlShippedVariant(experiment) {
|
|
125
311
|
return experiment.status === "stopped" && experiment.shippedVariantKey !== null && experiment.shippedVariantKey !== experiment.variants[0]?.variantKey;
|
|
126
312
|
}
|
|
@@ -146,6 +332,49 @@ function fnv1a32(value) {
|
|
|
146
332
|
}
|
|
147
333
|
return hash >>> 0;
|
|
148
334
|
}
|
|
335
|
+
function addTemplateIssues({
|
|
336
|
+
experiment,
|
|
337
|
+
context
|
|
338
|
+
}) {
|
|
339
|
+
const isTemplate = isDynamicRoute(experiment.route);
|
|
340
|
+
const shape = pageRouteParamShape(experiment.route);
|
|
341
|
+
experiment.variants.forEach((variant, index) => {
|
|
342
|
+
if (isDynamicRoute(variant.route) !== isTemplate) {
|
|
343
|
+
context.addIssue({
|
|
344
|
+
code: "custom",
|
|
345
|
+
path: ["variants", index, "route"],
|
|
346
|
+
message: isTemplate ? `"${variant.route}" must be a template like the experiment's route "${experiment.route}"` : `"${variant.route}" is a template, but the experiment's route "${experiment.route}" is one page`
|
|
347
|
+
});
|
|
348
|
+
} else if (isTemplate && pageRouteParamShape(variant.route) !== shape) {
|
|
349
|
+
context.addIssue({
|
|
350
|
+
code: "custom",
|
|
351
|
+
path: ["variants", index, "route"],
|
|
352
|
+
message: `"${variant.route}" must use the same params as "${experiment.route}" (${shape})`
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
experiment.includedRoutes.forEach((route, index) => {
|
|
357
|
+
if (!isTemplate) {
|
|
358
|
+
context.addIssue({
|
|
359
|
+
code: "custom",
|
|
360
|
+
path: ["includedRoutes"],
|
|
361
|
+
message: "includedRoutes only applies to a template route"
|
|
362
|
+
});
|
|
363
|
+
} else if (isDynamicRoute(route) || !matchPageRouteParams(experiment.route, normalizeRoute(route))) {
|
|
364
|
+
context.addIssue({
|
|
365
|
+
code: "custom",
|
|
366
|
+
path: ["includedRoutes", index],
|
|
367
|
+
message: `"${route}" isn't a page "${experiment.route}" renders`
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
addDuplicateIssues({
|
|
372
|
+
context,
|
|
373
|
+
values: experiment.includedRoutes.map(normalizeRoute),
|
|
374
|
+
path: ["includedRoutes"],
|
|
375
|
+
describe: (route) => `duplicate included route "${route}"`
|
|
376
|
+
});
|
|
377
|
+
}
|
|
149
378
|
function addDuplicateIssues({
|
|
150
379
|
context,
|
|
151
380
|
values,
|
|
@@ -161,4 +390,4 @@ function addDuplicateIssues({
|
|
|
161
390
|
}
|
|
162
391
|
}
|
|
163
392
|
|
|
164
|
-
export { SITE_KV_BINDING, SITE_KV_EXPERIMENTS_KEY, assignVariant, experimentKeySchema, experimentRouteSchema, experimentSchema, experimentStatusSchema, experimentVariantSchema, experimentsFileSchema, findActiveExperiment, findServedVariants, hasNonControlShippedVariant, isRoutingExperiment, normalizeRoute };
|
|
393
|
+
export { SITE_KV_BINDING, SITE_KV_EXPERIMENTS_KEY, assignVariant, experimentCoversPath, experimentKeySchema, experimentRouteSchema, experimentSchema, experimentStatusSchema, experimentVariantSchema, experimentsFileSchema, findActiveExperiment, findServedVariants, hasNonControlShippedVariant, isRoutingExperiment, isVariantPath, normalizeRoute, variantPathFor };
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-
|
|
5
|
+
"commit": "aa9296f4104e4be2271247d775a27fa69288bbe8",
|
|
6
|
+
"builtAt": "2026-11-02T03:31:48.722Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.18.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-
|
|
8
|
+
"commit": "aa9296f4104e4be2271247d775a27fa69288bbe8",
|
|
9
|
+
"builtAt": "2026-11-02T03:31:48.722Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|
package/routing/experiments.js
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
findActiveExperiment,
|
|
14
14
|
findServedVariants,
|
|
15
15
|
isRoutingExperiment,
|
|
16
|
-
|
|
16
|
+
isVariantPath,
|
|
17
|
+
normalizeRoute,
|
|
18
|
+
variantPathFor
|
|
17
19
|
} from "../_vendor/schemas/routing/experiments.mjs";
|
|
18
20
|
import { isConsentGranted } from "../consent/cookie";
|
|
19
21
|
import { ensureVisitorId } from "./visitor-id";
|
|
@@ -27,17 +29,19 @@ function decideExperiment({
|
|
|
27
29
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
28
30
|
return null;
|
|
29
31
|
}
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
+
const routing = parseExperiments(rawExperiments);
|
|
33
|
+
if (!routing) {
|
|
32
34
|
return null;
|
|
33
35
|
}
|
|
36
|
+
const { experiments, liveRoutes } = routing;
|
|
34
37
|
const isPreview = process.env.NODE_ENV === "development";
|
|
35
38
|
if (isPreview && !request.nextUrl.searchParams.has(EXPERIMENT_OVERRIDE_PARAM)) {
|
|
36
39
|
return null;
|
|
37
40
|
}
|
|
38
41
|
const experiment = findActiveExperiment({
|
|
39
42
|
experiments,
|
|
40
|
-
pathname: logicalPath
|
|
43
|
+
pathname: logicalPath,
|
|
44
|
+
liveRoutes
|
|
41
45
|
});
|
|
42
46
|
if (!experiment) {
|
|
43
47
|
return logicalPath.startsWith("/v/") && isChallengerRoute({ experiments, logicalPath }) ? { type: "variantRoute", servedPath: null } : null;
|
|
@@ -48,7 +52,11 @@ function decideExperiment({
|
|
|
48
52
|
});
|
|
49
53
|
return shippedVariant ? {
|
|
50
54
|
type: "shippedVariant",
|
|
51
|
-
servedPath: servedPathFor({
|
|
55
|
+
servedPath: servedPathFor({
|
|
56
|
+
experiment,
|
|
57
|
+
variant: shippedVariant,
|
|
58
|
+
logicalPath
|
|
59
|
+
})
|
|
52
60
|
} : null;
|
|
53
61
|
}
|
|
54
62
|
if (!isConsentGranted({ cookies: request.cookies, requiredConsent })) {
|
|
@@ -68,7 +76,7 @@ function decideExperiment({
|
|
|
68
76
|
const variant = forcedVariant ?? keptVariant ?? assignVariant({ visitorId, experiment });
|
|
69
77
|
return {
|
|
70
78
|
type: "assigned",
|
|
71
|
-
servedPath: servedPathFor({ variant, logicalPath }),
|
|
79
|
+
servedPath: servedPathFor({ experiment, variant, logicalPath }),
|
|
72
80
|
visitorId,
|
|
73
81
|
assignments: {
|
|
74
82
|
...keepRunningAssignments({ experiments, assignments: existing }),
|
|
@@ -140,14 +148,17 @@ function parseExperiments(rawExperiments) {
|
|
|
140
148
|
return parsedFiles.get(rawExperiments) ?? null;
|
|
141
149
|
}
|
|
142
150
|
const parsed = experimentsFileSchema.safeParse(rawExperiments);
|
|
143
|
-
const
|
|
144
|
-
experiments:
|
|
151
|
+
const routing = parsed.success ? {
|
|
152
|
+
experiments: withLivePagesOnly({
|
|
153
|
+
experiments: parsed.data.experiments,
|
|
154
|
+
liveRoutes: parsed.data.liveRoutes
|
|
155
|
+
}),
|
|
145
156
|
liveRoutes: parsed.data.liveRoutes
|
|
146
|
-
}
|
|
157
|
+
} : null;
|
|
147
158
|
if (isObject) {
|
|
148
|
-
parsedFiles.set(rawExperiments,
|
|
159
|
+
parsedFiles.set(rawExperiments, routing);
|
|
149
160
|
}
|
|
150
|
-
return
|
|
161
|
+
return routing;
|
|
151
162
|
}
|
|
152
163
|
function withLivePagesOnly({
|
|
153
164
|
experiments,
|
|
@@ -196,18 +207,19 @@ function isChallengerRoute({
|
|
|
196
207
|
experiments,
|
|
197
208
|
logicalPath
|
|
198
209
|
}) {
|
|
199
|
-
const route = normalizeRoute(logicalPath);
|
|
200
210
|
return experiments.filter(isRoutingExperiment).some((experiment) => {
|
|
201
211
|
return experiment.variants.slice(1).some((variant) => {
|
|
202
|
-
return
|
|
212
|
+
return isVariantPath({ variant, pathname: logicalPath });
|
|
203
213
|
});
|
|
204
214
|
});
|
|
205
215
|
}
|
|
206
216
|
function servedPathFor({
|
|
217
|
+
experiment,
|
|
207
218
|
variant,
|
|
208
219
|
logicalPath
|
|
209
220
|
}) {
|
|
210
|
-
|
|
221
|
+
const path = variantPathFor({ experiment, variant, pathname: logicalPath });
|
|
222
|
+
return path === null || normalizeRoute(path) === normalizeRoute(logicalPath) ? null : path;
|
|
211
223
|
}
|
|
212
224
|
export {
|
|
213
225
|
applyExperimentDecision,
|