next-evolve 0.0.0-stage → 0.3.1

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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -2
  3. package/dist/_virtual/_rolldown/runtime.js +12 -0
  4. package/dist/aggregate/attribution.d.ts +40 -0
  5. package/dist/aggregate/attribution.js +115 -0
  6. package/dist/aggregate/run.d.ts +21 -0
  7. package/dist/aggregate/run.js +139 -0
  8. package/dist/cli/aggregate.d.ts +9 -0
  9. package/dist/cli/aggregate.js +104 -0
  10. package/dist/cli/args.d.ts +12 -0
  11. package/dist/cli/args.js +23 -0
  12. package/dist/cli/cloud.d.ts +41 -0
  13. package/dist/cli/cloud.js +74 -0
  14. package/dist/cli/connect.d.ts +8 -0
  15. package/dist/cli/connect.js +46 -0
  16. package/dist/cli/detect.d.ts +26 -0
  17. package/dist/cli/detect.js +97 -0
  18. package/dist/cli/doctor.d.ts +34 -0
  19. package/dist/cli/doctor.js +230 -0
  20. package/dist/cli/env-file.d.ts +4 -0
  21. package/dist/cli/env-file.js +15 -0
  22. package/dist/cli/files.d.ts +25 -0
  23. package/dist/cli/files.js +173 -0
  24. package/dist/cli/index.d.ts +1 -0
  25. package/dist/cli/index.js +81 -0
  26. package/dist/cli/init.d.ts +26 -0
  27. package/dist/cli/init.js +240 -0
  28. package/dist/cli/load-app.d.ts +19 -0
  29. package/dist/cli/load-app.js +67 -0
  30. package/dist/cli/migrate.d.ts +7 -0
  31. package/dist/cli/migrate.js +30 -0
  32. package/dist/cli/pages.d.ts +17 -0
  33. package/dist/cli/pages.js +304 -0
  34. package/dist/cli/plan.d.ts +27 -0
  35. package/dist/cli/plan.js +129 -0
  36. package/dist/cli/pull.d.ts +13 -0
  37. package/dist/cli/pull.js +62 -0
  38. package/dist/cli/push.d.ts +14 -0
  39. package/dist/cli/push.js +33 -0
  40. package/dist/client/beacon.d.ts +11 -0
  41. package/dist/client/beacon.js +51 -0
  42. package/dist/client/evolve-script.d.ts +6 -0
  43. package/dist/client/evolve-script.js +60 -0
  44. package/dist/client/index.d.ts +3 -0
  45. package/dist/client/index.js +3 -0
  46. package/dist/cloud/manifest.d.ts +14 -0
  47. package/dist/cloud/manifest.js +46 -0
  48. package/dist/cloud/push.d.ts +21 -0
  49. package/dist/cloud/push.js +56 -0
  50. package/dist/cloud/regions.d.ts +8 -0
  51. package/dist/cloud/regions.js +25 -0
  52. package/dist/config/config.d.ts +51 -0
  53. package/dist/config/config.js +47 -0
  54. package/dist/config/goals.d.ts +50 -0
  55. package/dist/config/goals.js +85 -0
  56. package/dist/config/index.d.ts +3 -0
  57. package/dist/config/index.js +3 -0
  58. package/dist/core/assign.d.ts +35 -0
  59. package/dist/core/assign.js +104 -0
  60. package/dist/core/bots.d.ts +5 -0
  61. package/dist/core/bots.js +25 -0
  62. package/dist/core/constants.d.ts +3 -0
  63. package/dist/core/constants.js +5 -0
  64. package/dist/core/cookies.d.ts +12 -0
  65. package/dist/core/cookies.js +25 -0
  66. package/dist/core/keys.d.ts +14 -0
  67. package/dist/core/keys.js +38 -0
  68. package/dist/core/log.d.ts +2 -0
  69. package/dist/core/log.js +10 -0
  70. package/dist/db/client.d.ts +22 -0
  71. package/dist/db/client.js +50 -0
  72. package/dist/db/migrate.d.ts +18 -0
  73. package/dist/db/migrate.js +43 -0
  74. package/dist/db/writer.d.ts +53 -0
  75. package/dist/db/writer.js +182 -0
  76. package/dist/index.d.ts +5 -0
  77. package/dist/index.js +5 -0
  78. package/dist/instrumentation/index.d.ts +24 -0
  79. package/dist/instrumentation/index.js +54 -0
  80. package/dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.d.ts +11169 -0
  81. package/dist/proxy/index.d.ts +2 -0
  82. package/dist/proxy/index.js +1 -0
  83. package/dist/proxy/proxy.d.ts +24 -0
  84. package/dist/proxy/proxy.js +120 -0
  85. package/dist/react/context.d.ts +6 -0
  86. package/dist/react/context.js +8 -0
  87. package/dist/react/optimizable.d.ts +37 -0
  88. package/dist/react/optimizable.js +38 -0
  89. package/dist/react/region.d.ts +17 -0
  90. package/dist/react/region.js +127 -0
  91. package/dist/react/render-bundle.d.ts +15 -0
  92. package/dist/react/render-bundle.js +43 -0
  93. package/dist/react/with-evolve.d.ts +29 -0
  94. package/dist/react/with-evolve.js +57 -0
  95. package/dist/server/aggregate-route.d.ts +16 -0
  96. package/dist/server/aggregate-route.js +68 -0
  97. package/dist/server/collect.d.ts +62 -0
  98. package/dist/server/collect.js +157 -0
  99. package/dist/server/events.d.ts +18 -0
  100. package/dist/server/events.js +50 -0
  101. package/dist/server/evolve.d.ts +20 -0
  102. package/dist/server/evolve.js +57 -0
  103. package/dist/server/handler.d.ts +17 -0
  104. package/dist/server/handler.js +105 -0
  105. package/dist/server/index.d.ts +4 -0
  106. package/dist/server/index.js +4 -0
  107. package/dist/server/runtime.d.ts +26 -0
  108. package/dist/server/runtime.js +45 -0
  109. package/dist/server/sync-route.d.ts +28 -0
  110. package/dist/server/sync-route.js +100 -0
  111. package/dist/server/track-route.d.ts +25 -0
  112. package/dist/server/track-route.js +49 -0
  113. package/dist/store/disk.d.ts +6 -0
  114. package/dist/store/disk.js +23 -0
  115. package/dist/store/fetch.d.ts +26 -0
  116. package/dist/store/fetch.js +74 -0
  117. package/dist/store/global-config.d.ts +39 -0
  118. package/dist/store/global-config.js +122 -0
  119. package/dist/store/memory.d.ts +39 -0
  120. package/dist/store/memory.js +72 -0
  121. package/dist/store/runtime.d.ts +44 -0
  122. package/dist/store/runtime.js +101 -0
  123. package/migrations/0001_init.sql +56 -0
  124. package/package.json +73 -3
@@ -0,0 +1,2 @@
1
+ import { EvolveProxyOptions, applyEvolve, createEvolveProxy, evolveReady } from "./proxy.js";
2
+ export { type EvolveProxyOptions, applyEvolve, createEvolveProxy, evolveReady };
@@ -0,0 +1 @@
1
+ export { applyEvolve, createEvolveProxy, evolveReady } from "./proxy.js";
@@ -0,0 +1,24 @@
1
+ import { EvolveConfig } from "../config/config.js";
2
+ import { RuntimeOptions } from "../store/runtime.js";
3
+ import { NextFetchEvent, NextRequest, NextResponse } from "next/server";
4
+ export type EvolveProxyOptions = RuntimeOptions & {
5
+ /** Random source in [0, 1) (tests). */
6
+ random?: () => number;
7
+ };
8
+ /** Query parameter carrying a preview token from the dashboard. */
9
+ export declare const PREVIEW_PARAM = "evolve_preview";
10
+ /**
11
+ * Runs Evolve for one request. Returns a response for Evolve routes and
12
+ * `/ev_*` paths, or null for everything else (so it composes with an existing
13
+ * proxy: `return applyEvolve(config, req, event) ?? NextResponse.next()`).
14
+ * Never throws and never makes a network call on the request path.
15
+ */
16
+ export declare function applyEvolve(config: EvolveConfig, req: NextRequest, event?: Pick<NextFetchEvent, "waitUntil">, opts?: EvolveProxyOptions): NextResponse | null;
17
+ /** Builds the `proxy` export for `proxy.ts` (Next 16) or `middleware.ts` (Next 15.5, Node runtime). */
18
+ export declare function createEvolveProxy(config: EvolveConfig, opts?: EvolveProxyOptions): (req: NextRequest, event: NextFetchEvent) => NextResponse | Promise<NextResponse>;
19
+ /**
20
+ * With Global Config, an instance's first request waits ≤ 20 ms for one read
21
+ * (spec §7.8). Returns null when there's nothing to wait for. With an existing
22
+ * proxy: `await evolveReady(config)` before `applyEvolve(...)`.
23
+ */
24
+ export declare function evolveReady(config: EvolveConfig, opts?: RuntimeOptions): Promise<void> | null;
@@ -0,0 +1,120 @@
1
+ import { normalizePath, resolveConfig } from "../config/config.js";
2
+ import { assign, visitorDraw } from "../core/assign.js";
3
+ import { createBotDetector } from "../core/bots.js";
4
+ import { HEADER_CODE, HEADER_PATH } from "../core/constants.js";
5
+ import { ANON_COOKIE, ASSIGN_COOKIE, PREVIEW_COOKIE, parseAssignments, serializeAssignments } from "../core/cookies.js";
6
+ import { logRateLimited } from "../core/log.js";
7
+ import { coldStart, getBundleStore, getSiteIdentity } from "../store/runtime.js";
8
+ import { CONTROL_CODE, encodeCode, newAnonId, verifyPreview } from "@next-evolve/protocol";
9
+ import { NextResponse } from "next/server";
10
+ // src/proxy/proxy.ts
11
+ /** Query parameter carrying a preview token from the dashboard. */
12
+ const PREVIEW_PARAM = "evolve_preview";
13
+ const detectors = /* @__PURE__ */ new WeakMap();
14
+ /**
15
+ * Runs Evolve for one request. Returns a response for Evolve routes and
16
+ * `/ev_*` paths, or null for everything else (so it composes with an existing
17
+ * proxy: `return applyEvolve(config, req, event) ?? NextResponse.next()`).
18
+ * Never throws and never makes a network call on the request path.
19
+ */
20
+ function applyEvolve(config, req, event, opts = {}) {
21
+ const t0 = performance.now();
22
+ const pathname = req.nextUrl.pathname;
23
+ if (pathname.split("/")[1]?.startsWith("ev_")) return new NextResponse(null, { status: 404 });
24
+ let cfg;
25
+ try {
26
+ cfg = resolveConfig(config);
27
+ } catch (e) {
28
+ logRateLimited("config", "invalid evolve config", e);
29
+ return null;
30
+ }
31
+ const path = normalizePath(pathname);
32
+ const route = cfg.routes.get(path);
33
+ if (!route) return null;
34
+ const isStatic = route.mode === "static";
35
+ let res;
36
+ try {
37
+ let isBot = detectors.get(config);
38
+ if (!isBot) {
39
+ isBot = createBotDetector(cfg.botPatterns);
40
+ detectors.set(config, isBot);
41
+ }
42
+ const bundle = getBundleStore(cfg, opts)?.peek(event ? (p) => event.waitUntil(p) : void 0) ?? null;
43
+ let code = CONTROL_CODE;
44
+ const setCookies = [];
45
+ const fromQuery = req.nextUrl.searchParams.get(PREVIEW_PARAM);
46
+ const previewToken = fromQuery ?? req.cookies.get("evolve_pv")?.value;
47
+ const identity = previewToken ? getSiteIdentity(opts) : null;
48
+ const preview = previewToken && identity ? verifyPreview(previewToken, identity) : null;
49
+ if (preview) {
50
+ code = preview.code;
51
+ if (fromQuery) setCookies.push([
52
+ PREVIEW_COOKIE,
53
+ fromQuery,
54
+ true
55
+ ]);
56
+ } else if (bundle && !isBot(req.headers.get("user-agent")) && cfg.consent?.(req) !== false) {
57
+ const cookie = parseAssignments(req.cookies.get(ASSIGN_COOKIE)?.value);
58
+ const known = req.cookies.get(ANON_COOKIE)?.value;
59
+ const anon = known || newAnonId();
60
+ const a = assign(bundle, path, cookie, opts.random ?? visitorDraw(anon));
61
+ code = encodeCode(a.pairs);
62
+ if ((bundle.routing.routes[path] ?? []).some((e) => bundle.routing.experiments[e]?.status === "running")) {
63
+ if (!known) setCookies.push([ANON_COOKIE, anon]);
64
+ if (a.changed) setCookies.push([ASSIGN_COOKIE, serializeAssignments(a.assigned)]);
65
+ }
66
+ }
67
+ res = isStatic ? rewrite(req, code, path) : passThrough(req, code, path);
68
+ if (setCookies.length) {
69
+ const secure = (req.headers.get("x-forwarded-proto") ?? req.nextUrl.protocol.replace(":", "")) === "https";
70
+ const maxAge = cfg.cookies.maxAgeDays * 86400;
71
+ for (const [name, value, session] of setCookies) res.cookies.set(name, value, {
72
+ path: "/",
73
+ ...session ? {} : { maxAge },
74
+ sameSite: "lax",
75
+ httpOnly: true,
76
+ secure,
77
+ ...cfg.cookies.domain ? { domain: cfg.cookies.domain } : {}
78
+ });
79
+ }
80
+ } catch (e) {
81
+ logRateLimited("proxy", "proxy error; serving control", e);
82
+ res = isStatic ? rewrite(req, CONTROL_CODE, path) : passThrough(req, CONTROL_CODE, path);
83
+ }
84
+ if ((opts.env ?? process.env).VERCEL !== "1") res.headers.set("cache-control", "private, no-store");
85
+ res.headers.set("server-timing", `evolve;dur=${(performance.now() - t0).toFixed(3)}`);
86
+ return res;
87
+ }
88
+ function rewrite(req, code, path) {
89
+ const target = path === "/" ? `/${code}` : `/${code}${path}`;
90
+ return NextResponse.rewrite(new URL(target + req.nextUrl.search, req.url));
91
+ }
92
+ function passThrough(req, code, path) {
93
+ const headers = new Headers(req.headers);
94
+ headers.set(HEADER_CODE, code);
95
+ headers.set(HEADER_PATH, path);
96
+ return NextResponse.next({ request: { headers } });
97
+ }
98
+ /** Builds the `proxy` export for `proxy.ts` (Next 16) or `middleware.ts` (Next 15.5, Node runtime). */
99
+ function createEvolveProxy(config, opts = {}) {
100
+ resolveConfig(config);
101
+ return function proxy(req, event) {
102
+ const ready = evolveReady(config, opts);
103
+ const run = () => applyEvolve(config, req, event, opts) ?? NextResponse.next();
104
+ return ready ? ready.then(run) : run();
105
+ };
106
+ }
107
+ /**
108
+ * With Global Config, an instance's first request waits ≤ 20 ms for one read
109
+ * (spec §7.8). Returns null when there's nothing to wait for. With an existing
110
+ * proxy: `await evolveReady(config)` before `applyEvolve(...)`.
111
+ */
112
+ function evolveReady(config, opts = {}) {
113
+ try {
114
+ const store = getBundleStore(resolveConfig(config), opts);
115
+ return store ? coldStart(store) : null;
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+ export { PREVIEW_PARAM, applyEvolve, createEvolveProxy, evolveReady };
@@ -0,0 +1,6 @@
1
+ export type EvolveRequestContext = {
2
+ code: string | null;
3
+ path: string | null;
4
+ };
5
+ /** Per-request store (React `cache()`), filled by `withEvolve` before the page body renders. */
6
+ export declare const requestContext: () => EvolveRequestContext;
@@ -0,0 +1,8 @@
1
+ import { cache } from "react";
2
+ // src/react/context.ts
3
+ /** Per-request store (React `cache()`), filled by `withEvolve` before the page body renders. */
4
+ const requestContext = cache(() => ({
5
+ code: null,
6
+ path: null
7
+ }));
8
+ export { requestContext };
@@ -0,0 +1,37 @@
1
+ import { ComponentType } from "react";
2
+ import { z } from "zod";
3
+ declare const META: unique symbol;
4
+ export type OptimizableMeta<S extends z.ZodObject = z.ZodObject> = {
5
+ schema: S;
6
+ /** JSON Schema of the editable props (dashboard forms; later the AI). Includes `.meta()`. */
7
+ jsonSchema: Record<string, unknown>;
8
+ /** sha256 of the canonical JSON Schema; changes when the schema changes. */
9
+ schemaHash: string;
10
+ keys: readonly string[];
11
+ };
12
+ export type OptimizableComponent<P extends object, S extends z.ZodObject = z.ZodObject> = ComponentType<P> & {
13
+ readonly [META]: OptimizableMeta<S>;
14
+ };
15
+ /**
16
+ * Compile-time check that the schema fits the component: every schema key is a
17
+ * prop the component accepts, with a compatible type.
18
+ */
19
+ type SchemaFits<P, O> = [Exclude<keyof O, keyof P>] extends [never] ? O extends Pick<P, keyof O & keyof P> ? unknown : {
20
+ "schema error": "schema prop types don't match the component's props";
21
+ } : {
22
+ "schema error": "schema has props the component doesn't accept";
23
+ keys: Exclude<keyof O, keyof P>;
24
+ };
25
+ /**
26
+ * Marks a component as optimizable with a Zod schema for the props Evolve may
27
+ * change. A schema that doesn't fit the component's props fails to compile.
28
+ * Server-only (the component renders inside `<Optimizable>`).
29
+ *
30
+ * ```ts
31
+ * export default optimizable(Hero, heroSchema);
32
+ * ```
33
+ */
34
+ export declare function optimizable<P extends object, S extends z.ZodObject>(component: ComponentType<P>, schema: S & SchemaFits<P, z.output<S>>): OptimizableComponent<P, S>;
35
+ export declare function getOptimizableMeta(c: unknown): OptimizableMeta | null;
36
+ export declare function isRegionId(id: string): boolean;
37
+ export {};
@@ -0,0 +1,38 @@
1
+ import { REGION_ID_RE, canonicalJson, sha256Hex } from "@next-evolve/protocol";
2
+ import { createElement } from "react";
3
+ import { z } from "zod";
4
+ // src/react/optimizable.ts
5
+ const META = Symbol.for("next-evolve.optimizable");
6
+ /**
7
+ * Marks a component as optimizable with a Zod schema for the props Evolve may
8
+ * change. A schema that doesn't fit the component's props fails to compile.
9
+ * Server-only (the component renders inside `<Optimizable>`).
10
+ *
11
+ * ```ts
12
+ * export default optimizable(Hero, heroSchema);
13
+ * ```
14
+ */
15
+ function optimizable(component, schema) {
16
+ const jsonSchema = z.toJSONSchema(schema, {
17
+ io: "input",
18
+ unrepresentable: "any"
19
+ });
20
+ const meta = {
21
+ schema,
22
+ jsonSchema,
23
+ schemaHash: sha256Hex(canonicalJson(jsonSchema)),
24
+ keys: Object.keys(schema.shape)
25
+ };
26
+ const Wrapped = ((props) => createElement(component, props));
27
+ Object.defineProperty(Wrapped, META, { value: meta });
28
+ Object.defineProperty(Wrapped, "displayName", { value: `Optimizable(${component.displayName ?? component.name ?? "Component"})` });
29
+ Object.defineProperty(Wrapped, "inner", { value: component });
30
+ return Wrapped;
31
+ }
32
+ function getOptimizableMeta(c) {
33
+ return c?.[META] ?? null;
34
+ }
35
+ function isRegionId(id) {
36
+ return REGION_ID_RE.test(id);
37
+ }
38
+ export { getOptimizableMeta, isRegionId, optimizable };
@@ -0,0 +1,17 @@
1
+ import { OptimizableComponent } from "./optimizable.js";
2
+ import { ReactElement } from "react";
3
+ import { z } from "zod";
4
+ /** Control props: the schema's input type (defaults optional) plus the component's other props. */
5
+ export type ControlProps<P extends object, S extends z.ZodObject> = Omit<P, keyof z.output<S>> & z.input<S>;
6
+ export type OptimizableProps<P extends object, S extends z.ZodObject> = {
7
+ /** Region id, unique per site: /^[a-z0-9][a-z0-9-]{0,39}$/. */
8
+ id: string;
9
+ component: OptimizableComponent<P, S>;
10
+ /** Control props. Variant props are merged over these; schema defaults apply. */
11
+ props: ControlProps<P, S>;
12
+ };
13
+ /**
14
+ * Renders the variant of a region chosen by the proxy, or control. Never throws
15
+ * because of Evolve: any problem renders control. Server component.
16
+ */
17
+ export declare function Optimizable<P extends object, S extends z.ZodObject>({ id, component, props }: OptimizableProps<P, S>): Promise<ReactElement>;
@@ -0,0 +1,127 @@
1
+ import { registerRegion } from "../cloud/regions.js";
2
+ import { HEADER_CODE, HEADER_PATH } from "../core/constants.js";
3
+ import { logRateLimited } from "../core/log.js";
4
+ import { requestContext } from "./context.js";
5
+ import { getOptimizableMeta } from "./optimizable.js";
6
+ import { getRenderBundle } from "./render-bundle.js";
7
+ import { CONTROL_CODE, decodeCode } from "@next-evolve/protocol";
8
+ import { createElement } from "react";
9
+ import { headers } from "next/headers";
10
+ import { unstable_rethrow } from "next/navigation";
11
+ import { after } from "next/server";
12
+ // src/react/region.tsx
13
+ let warnedNoContext = false;
14
+ async function currentCode() {
15
+ const ctx = requestContext();
16
+ if (ctx.code) return ctx.code;
17
+ try {
18
+ return (await headers()).get(HEADER_CODE);
19
+ } catch (e) {
20
+ unstable_rethrow(e);
21
+ return null;
22
+ }
23
+ }
24
+ /** The page path this region renders on (static: withEvolve; dynamic: the proxy header). */
25
+ async function currentPath() {
26
+ const ctx = requestContext();
27
+ if (ctx.path) return ctx.path;
28
+ try {
29
+ return (await headers()).get(HEADER_PATH);
30
+ } catch (e) {
31
+ unstable_rethrow(e);
32
+ return null;
33
+ }
34
+ }
35
+ /** Region registration (spec §5.2): after the response, deduped per process, errors swallowed. */
36
+ function scheduleRegistration(id, meta, controlProps, path) {
37
+ if (!path) return;
38
+ try {
39
+ after(() => registerRegion({
40
+ id,
41
+ jsonSchema: meta.jsonSchema,
42
+ schemaHash: meta.schemaHash,
43
+ controlProps,
44
+ path
45
+ }));
46
+ } catch {}
47
+ }
48
+ function findPair(bundle, pairs, region) {
49
+ for (const p of pairs) {
50
+ const v = bundle?.variants[p.var];
51
+ if (v && v.exp === p.exp && v.region === region) return {
52
+ pair: p,
53
+ variant: v
54
+ };
55
+ }
56
+ return null;
57
+ }
58
+ /**
59
+ * Renders the variant of a region chosen by the proxy, or control. Never throws
60
+ * because of Evolve: any problem renders control. Server component.
61
+ */
62
+ async function Optimizable({ id, component, props }) {
63
+ const meta = getOptimizableMeta(component);
64
+ if (!meta) {
65
+ logRateLimited(`optimizable:${id}`, `<Optimizable id="${id}">: component isn't wrapped with optimizable()`);
66
+ return createElement(component, props);
67
+ }
68
+ const editableOf = (o) => {
69
+ const out = {};
70
+ for (const k of meta.keys) if (k in o) out[k] = o[k];
71
+ return out;
72
+ };
73
+ const parsedControl = meta.schema.safeParse(editableOf(props));
74
+ const controlProps = {
75
+ ...props,
76
+ ...parsedControl.success ? parsedControl.data : {}
77
+ };
78
+ const control = () => createElement(component, controlProps);
79
+ const code = await currentCode();
80
+ scheduleRegistration(id, meta, parsedControl.success ? parsedControl.data : editableOf(props), await currentPath());
81
+ if (!code) {
82
+ if (!warnedNoContext && process.env.NODE_ENV !== "production") {
83
+ warnedNoContext = true;
84
+ console.warn(`[evolve] <Optimizable id="${id}"> rendered without Evolve context; serving control. Wrap the page with withEvolve() under app/[evolve]/, or use a dynamic route.`);
85
+ }
86
+ return control();
87
+ }
88
+ if (code === CONTROL_CODE) return control();
89
+ const pairs = decodeCode(code);
90
+ if (!pairs || pairs.length === 0) return control();
91
+ let bundle = await getRenderBundle();
92
+ let hit = findPair(bundle, pairs, id);
93
+ const unknown = () => pairs.some((p) => !bundle?.variants[p.var]);
94
+ if (!hit && unknown()) {
95
+ bundle = await getRenderBundle({ fresh: true });
96
+ hit = findPair(bundle, pairs, id);
97
+ }
98
+ const marker = (attrs, child) => createElement("div", {
99
+ style: { display: "contents" },
100
+ "data-evolve-r": id,
101
+ ...attrs
102
+ }, child);
103
+ if (!hit) return unknown() ? marker({ "data-evolve-x": "" }, control()) : control();
104
+ const { pair, variant } = hit;
105
+ const attrs = {
106
+ "data-evolve-e": pair.exp,
107
+ "data-evolve-v": pair.var
108
+ };
109
+ if (!pair.counted) attrs["data-evolve-x"] = "";
110
+ if (variant.ctrl || Object.keys(variant.props).length === 0) return marker(attrs, control());
111
+ const parsed = meta.schema.safeParse(editableOf({
112
+ ...props,
113
+ ...variant.props
114
+ }));
115
+ if (!parsed.success) {
116
+ logRateLimited(`variant:${pair.var}`, `variant ${pair.var} of region "${id}" failed its schema; serving control`, parsed.error.issues);
117
+ return marker({
118
+ ...attrs,
119
+ "data-evolve-x": ""
120
+ }, control());
121
+ }
122
+ return marker(attrs, createElement(component, {
123
+ ...props,
124
+ ...parsed.data
125
+ }));
126
+ }
127
+ export { Optimizable };
@@ -0,0 +1,15 @@
1
+ import { BundlePayload } from "@next-evolve/protocol";
2
+ type Env = Record<string, string | undefined>;
3
+ /**
4
+ * Bundle for rendering (spec §7.7). Warm: the process-wide store (shared with
5
+ * the proxy where they run in one process). Cold (empty store, e.g. a fresh
6
+ * serverless render function): `GET /v1/bundle` through Next's data cache,
7
+ * tagged `evolve-bundle`, so instances share it and the sync route's
8
+ * `revalidateTag` drops it. `fresh` pulls uncached, for a variant code the
9
+ * cached bundle doesn't know yet (a new or preview-only variant).
10
+ */
11
+ export declare function getRenderBundle(opts?: {
12
+ fresh?: boolean;
13
+ env?: Env;
14
+ }): Promise<BundlePayload | null>;
15
+ export {};
@@ -0,0 +1,43 @@
1
+ import { BUNDLE_TAG } from "../server/sync-route.js";
2
+ import { getBundleStore } from "../store/runtime.js";
3
+ // src/react/render-bundle.ts
4
+ /**
5
+ * Bundle for rendering (spec §7.7). Warm: the process-wide store (shared with
6
+ * the proxy where they run in one process). Cold (empty store, e.g. a fresh
7
+ * serverless render function): `GET /v1/bundle` through Next's data cache,
8
+ * tagged `evolve-bundle`, so instances share it and the sync route's
9
+ * `revalidateTag` drops it. `fresh` pulls uncached, for a variant code the
10
+ * cached bundle doesn't know yet (a new or preview-only variant).
11
+ */
12
+ async function getRenderBundle(opts = {}) {
13
+ const env = opts.env ?? process.env;
14
+ const store = getBundleStore({}, { env });
15
+ if (!store) return null;
16
+ if (opts.fresh) {
17
+ await store.refresh({ force: true });
18
+ return store.peek();
19
+ }
20
+ const warm = store.peek();
21
+ if (warm) return warm;
22
+ const token = await taggedFetch(env);
23
+ if (token) store.load(token);
24
+ return store.peek();
25
+ }
26
+ async function taggedFetch(env) {
27
+ const cloudUrl = env.EVOLVE_CLOUD_URL ?? "https://api.evolveab.com";
28
+ const key = env.EVOLVE_SECRET_KEY;
29
+ if (!cloudUrl || !key) return null;
30
+ try {
31
+ const res = await fetch(new URL("/v1/bundle", cloudUrl), {
32
+ headers: { authorization: `Bearer ${key}` },
33
+ cache: "force-cache",
34
+ next: { tags: [BUNDLE_TAG] },
35
+ signal: AbortSignal.timeout(5e3)
36
+ });
37
+ if (!res.ok) return null;
38
+ return (await res.text()).trim() || null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ export { getRenderBundle };
@@ -0,0 +1,29 @@
1
+ import { ComponentType, ReactElement } from "react";
2
+ import { Metadata } from "next";
3
+ /** Props Next passes to a page under `app/[evolve]/`. */
4
+ export type EvolvePageProps = {
5
+ params: Promise<{
6
+ evolve: string;
7
+ } & Record<string, string | string[] | undefined>>;
8
+ searchParams?: Promise<Record<string, string | string[] | undefined>>;
9
+ };
10
+ /**
11
+ * Wraps a static-mode page under `app/[evolve]/<route>/page.tsx`. Decodes the
12
+ * variant code from `params.evolve` and makes it available to every
13
+ * `<Optimizable>` on the page before the body renders.
14
+ */
15
+ export declare function withEvolve<P extends object>(Page: ComponentType<P>, opts: {
16
+ path: string;
17
+ }): (props: P & EvolvePageProps) => Promise<ReactElement>;
18
+ /** `generateStaticParams` for `app/[evolve]/`: prerenders control only, so builds need no network. */
19
+ export declare function evolveStaticParams(): {
20
+ evolve: string;
21
+ }[];
22
+ type MetadataInput = Metadata | ((props: EvolvePageProps) => Metadata | Promise<Metadata>);
23
+ /**
24
+ * `generateMetadata` for a static-mode page: canonical is the original path
25
+ * for every variant, and variant renders are `noindex` (bots only ever get
26
+ * control anyway).
27
+ */
28
+ export declare function evolveMetadata(path: string, base?: MetadataInput): (props: EvolvePageProps) => Promise<Metadata>;
29
+ export {};
@@ -0,0 +1,57 @@
1
+ import { normalizePath } from "../config/config.js";
2
+ import { requestContext } from "./context.js";
3
+ import { CODE_RE, CONTROL_CODE } from "@next-evolve/protocol";
4
+ import { createElement } from "react";
5
+ import { notFound } from "next/navigation";
6
+ // src/react/with-evolve.ts
7
+ /** Validates the `[evolve]` segment; malformed codes are a 404. */
8
+ async function readCode(params) {
9
+ const { evolve } = await params;
10
+ if (typeof evolve !== "string" || !CODE_RE.test(evolve)) notFound();
11
+ return evolve;
12
+ }
13
+ /**
14
+ * Wraps a static-mode page under `app/[evolve]/<route>/page.tsx`. Decodes the
15
+ * variant code from `params.evolve` and makes it available to every
16
+ * `<Optimizable>` on the page before the body renders.
17
+ */
18
+ function withEvolve(Page, opts) {
19
+ const path = normalizePath(opts.path);
20
+ async function EvolvePage(props) {
21
+ const code = await readCode(props.params);
22
+ const ctx = requestContext();
23
+ ctx.code = code;
24
+ ctx.path = path;
25
+ return createElement(Page, props);
26
+ }
27
+ Object.defineProperty(EvolvePage, "name", { value: `withEvolve(${Page.displayName ?? Page.name ?? "Page"})` });
28
+ return EvolvePage;
29
+ }
30
+ /** `generateStaticParams` for `app/[evolve]/`: prerenders control only, so builds need no network. */
31
+ function evolveStaticParams() {
32
+ return [{ evolve: CONTROL_CODE }];
33
+ }
34
+ /**
35
+ * `generateMetadata` for a static-mode page: canonical is the original path
36
+ * for every variant, and variant renders are `noindex` (bots only ever get
37
+ * control anyway).
38
+ */
39
+ function evolveMetadata(path, base = {}) {
40
+ const canonical = normalizePath(path);
41
+ return async function generateMetadata(props) {
42
+ const code = await readCode(props.params);
43
+ const m = typeof base === "function" ? await base(props) : base;
44
+ return {
45
+ ...m,
46
+ alternates: {
47
+ ...m.alternates,
48
+ canonical
49
+ },
50
+ ...code === CONTROL_CODE ? {} : { robots: {
51
+ index: false,
52
+ follow: true
53
+ } }
54
+ };
55
+ };
56
+ }
57
+ export { evolveMetadata, evolveStaticParams, withEvolve };
@@ -0,0 +1,16 @@
1
+ import { ResolvedConfig } from "../config/config.js";
2
+ import { Sql } from "../db/client.js";
3
+ import { ReplayGuard } from "@next-evolve/protocol";
4
+ type Env = Record<string, string | undefined>;
5
+ export type AggregateRouteDeps = {
6
+ config: ResolvedConfig;
7
+ env: Env;
8
+ sql: () => Sql | null;
9
+ sdkVersion: string;
10
+ replay?: ReplayGuard;
11
+ now?: () => number;
12
+ };
13
+ /** Delivery ids seen in the last 10 minutes, shared by every signed cloud → app route. */
14
+ export declare function sharedReplayGuard(): ReplayGuard;
15
+ export declare function handleAggregate(req: Request, deps: AggregateRouteDeps): Promise<Response>;
16
+ export {};
@@ -0,0 +1,68 @@
1
+ import { runAggregate } from "../aggregate/run.js";
2
+ import { DELIVERY_HEADER, SIGNATURE_HEADER, aggregateRequestSchema, createReplayGuard, isSupportedProtocol, parseSecretKey, signBody, unsupportedProtocolBody, verifyBody } from "@next-evolve/protocol";
3
+ // src/server/aggregate-route.ts
4
+ const MAX_BODY_BYTES = 262144;
5
+ const KEY = Symbol.for("next-evolve.replay");
6
+ /** Delivery ids seen in the last 10 minutes, shared by every signed cloud → app route. */
7
+ function sharedReplayGuard() {
8
+ const g = globalThis;
9
+ g[KEY] ??= createReplayGuard();
10
+ return g[KEY];
11
+ }
12
+ const json = (status, body, headers = {}) => Response.json(body, {
13
+ status,
14
+ headers
15
+ });
16
+ async function handleAggregate(req, deps) {
17
+ const secret = deps.env.EVOLVE_WEBHOOK_SECRET;
18
+ const site = parseSecretKey(deps.env.EVOLVE_SECRET_KEY ?? "")?.siteId;
19
+ if (!secret || !site) return json(503, { error: "not_configured" });
20
+ const body = await req.text();
21
+ if (Buffer.byteLength(body) > MAX_BODY_BYTES) return json(413, { error: "too_large" });
22
+ const now = deps.now ?? Date.now;
23
+ const sig = verifyBody(req.headers.get(SIGNATURE_HEADER), body, secret, { now: Math.floor(now() / 1e3) });
24
+ if (!sig.ok) return json(401, {
25
+ error: "invalid_signature",
26
+ reason: sig.reason
27
+ });
28
+ const delivery = req.headers.get(DELIVERY_HEADER) ?? "";
29
+ if (!(deps.replay ?? sharedReplayGuard()).check(delivery)) return json(409, { error: "replayed" });
30
+ let parsed;
31
+ try {
32
+ parsed = JSON.parse(body);
33
+ } catch {
34
+ return json(400, { error: "invalid_json" });
35
+ }
36
+ if (!isSupportedProtocol(parsed?.pv)) return json(400, unsupportedProtocolBody());
37
+ const r = aggregateRequestSchema.safeParse(parsed);
38
+ if (!r.success) return json(400, { error: "invalid" });
39
+ if (r.data.site !== site) return json(403, { error: "site_mismatch" });
40
+ const sql = deps.sql();
41
+ if (!sql) return json(503, { error: "no_database" });
42
+ let result;
43
+ try {
44
+ result = await runAggregate(sql, r.data, {
45
+ maxMs: deps.config.aggregate.maxMs,
46
+ statementTimeoutMs: deps.config.aggregate.statementTimeoutMs,
47
+ sdkVersion: deps.sdkVersion,
48
+ now
49
+ });
50
+ } catch (e) {
51
+ console.error("[evolve] aggregate failed", e);
52
+ return json(503, { error: "database_error" });
53
+ }
54
+ if (result.kind === "busy") return json(429, {
55
+ busy: true,
56
+ retryAfter: result.retryAfter
57
+ }, { "retry-after": String(result.retryAfter) });
58
+ const out = JSON.stringify(result.response);
59
+ return new Response(out, {
60
+ status: 200,
61
+ headers: {
62
+ "content-type": "application/json",
63
+ "cache-control": "no-store",
64
+ [SIGNATURE_HEADER]: signBody(secret, out, Math.floor(now() / 1e3))
65
+ }
66
+ });
67
+ }
68
+ export { handleAggregate, sharedReplayGuard };