@traffical/svelte 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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @traffical/svelte - Unit Tests
3
+ *
4
+ * Tests for the Svelte 5 SDK hooks and utilities.
5
+ */
6
+ import { describe, test, expect } from "bun:test";
7
+ import { resolveParameters } from "@traffical/core";
8
+ // =============================================================================
9
+ // Test Fixtures
10
+ // =============================================================================
11
+ const mockBundle = {
12
+ version: new Date().toISOString(),
13
+ orgId: "org_test",
14
+ projectId: "proj_test",
15
+ env: "test",
16
+ hashing: {
17
+ unitKey: "userId",
18
+ bucketCount: 10000,
19
+ },
20
+ parameters: [
21
+ {
22
+ key: "checkout.ctaText",
23
+ type: "string",
24
+ default: "Buy Now",
25
+ layerId: "layer_1",
26
+ namespace: "checkout",
27
+ },
28
+ {
29
+ key: "checkout.ctaColor",
30
+ type: "string",
31
+ default: "#000000",
32
+ layerId: "layer_1",
33
+ namespace: "checkout",
34
+ },
35
+ {
36
+ key: "feature.newCheckout",
37
+ type: "boolean",
38
+ default: false,
39
+ layerId: "layer_2",
40
+ namespace: "feature",
41
+ },
42
+ ],
43
+ layers: [
44
+ {
45
+ id: "layer_1",
46
+ policies: [],
47
+ },
48
+ {
49
+ id: "layer_2",
50
+ policies: [],
51
+ },
52
+ ],
53
+ domBindings: [],
54
+ };
55
+ const mockBundleWithLayer = {
56
+ ...mockBundle,
57
+ layers: [
58
+ {
59
+ id: "layer_1",
60
+ policies: [
61
+ {
62
+ id: "policy_1",
63
+ state: "running",
64
+ kind: "static",
65
+ conditions: [],
66
+ allocations: [
67
+ {
68
+ id: "alloc_1",
69
+ name: "control",
70
+ bucketRange: [0, 4999],
71
+ overrides: {
72
+ "checkout.ctaText": "Buy Now",
73
+ "checkout.ctaColor": "#000000",
74
+ },
75
+ },
76
+ {
77
+ id: "alloc_2",
78
+ name: "treatment",
79
+ bucketRange: [5000, 9999],
80
+ overrides: {
81
+ "checkout.ctaText": "Purchase",
82
+ "checkout.ctaColor": "#FF0000",
83
+ },
84
+ },
85
+ ],
86
+ },
87
+ ],
88
+ },
89
+ {
90
+ id: "layer_2",
91
+ policies: [],
92
+ },
93
+ ],
94
+ };
95
+ // =============================================================================
96
+ // Resolution Tests
97
+ // =============================================================================
98
+ describe("resolveParameters", () => {
99
+ test("returns defaults when bundle is null", () => {
100
+ const defaults = {
101
+ "checkout.ctaText": "Default Text",
102
+ "checkout.ctaColor": "#FFFFFF",
103
+ };
104
+ const result = resolveParameters(null, {}, defaults);
105
+ expect(result).toEqual(defaults);
106
+ });
107
+ test("resolves parameters from bundle defaults", () => {
108
+ const defaults = {
109
+ "checkout.ctaText": "Fallback",
110
+ "checkout.ctaColor": "#FFFFFF",
111
+ };
112
+ const result = resolveParameters(mockBundle, { userId: "user_123" }, defaults);
113
+ expect(result["checkout.ctaText"]).toBe("Buy Now");
114
+ expect(result["checkout.ctaColor"]).toBe("#000000");
115
+ });
116
+ test("returns defaults for missing parameters", () => {
117
+ const defaults = {
118
+ "checkout.ctaText": "Fallback",
119
+ "nonexistent.param": "Default Value",
120
+ };
121
+ const result = resolveParameters(mockBundle, { userId: "user_123" }, defaults);
122
+ expect(result["checkout.ctaText"]).toBe("Buy Now");
123
+ expect(result["nonexistent.param"]).toBe("Default Value");
124
+ });
125
+ test("resolves boolean parameters correctly", () => {
126
+ const defaults = {
127
+ "feature.newCheckout": true, // Default to true, bundle has false
128
+ };
129
+ const result = resolveParameters(mockBundle, { userId: "user_123" }, defaults);
130
+ expect(result["feature.newCheckout"]).toBe(false);
131
+ });
132
+ });
133
+ // =============================================================================
134
+ // SSR Behavior Tests
135
+ // =============================================================================
136
+ describe("SSR behavior", () => {
137
+ test("isBrowser returns false in test environment", () => {
138
+ // In Bun test environment, window is not defined
139
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
140
+ expect(isBrowser).toBe(false);
141
+ });
142
+ test("resolveParameters works without browser APIs", () => {
143
+ // This verifies that core resolution doesn't depend on browser APIs
144
+ const defaults = {
145
+ "checkout.ctaText": "Fallback",
146
+ };
147
+ const result = resolveParameters(mockBundle, { userId: "ssr_user" }, defaults);
148
+ expect(result["checkout.ctaText"]).toBe("Buy Now");
149
+ });
150
+ });
151
+ // =============================================================================
152
+ // Type Safety Tests
153
+ // =============================================================================
154
+ describe("type safety", () => {
155
+ test("preserves type inference for defaults", () => {
156
+ const defaults = {
157
+ stringParam: "hello",
158
+ numberParam: 42,
159
+ booleanParam: true,
160
+ };
161
+ // Type check - this should compile
162
+ const result = resolveParameters(mockBundle, {}, defaults);
163
+ expect(typeof result.stringParam).toBe("string");
164
+ expect(typeof result.numberParam).toBe("number");
165
+ expect(typeof result.booleanParam).toBe("boolean");
166
+ });
167
+ });
168
+ // =============================================================================
169
+ // Bundle Validation Tests
170
+ // =============================================================================
171
+ describe("bundle structure", () => {
172
+ test("mock bundle has expected structure", () => {
173
+ expect(mockBundle.orgId).toBe("org_test");
174
+ expect(mockBundle.hashing.unitKey).toBe("userId");
175
+ expect(mockBundle.hashing.bucketCount).toBe(10000);
176
+ expect(mockBundle.parameters).toHaveLength(3);
177
+ expect(mockBundle.layers).toHaveLength(2);
178
+ });
179
+ test("mock bundle with layer has allocations", () => {
180
+ expect(mockBundleWithLayer.layers).toHaveLength(2);
181
+ expect(mockBundleWithLayer.layers[0].policies).toHaveLength(1);
182
+ expect(mockBundleWithLayer.layers[0].policies[0].allocations).toHaveLength(2);
183
+ });
184
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @traffical/svelte - SvelteKit Helpers
3
+ *
4
+ * Server-side utilities for SvelteKit load functions.
5
+ * Enables SSR with pre-fetched config bundles.
6
+ */
7
+ import type { ConfigBundle, Context, ParameterValue } from "@traffical/core";
8
+ import type { LoadTrafficalBundleOptions, LoadTrafficalBundleResult } from "./types.js";
9
+ /**
10
+ * Loads the Traffical config bundle in a SvelteKit load function.
11
+ *
12
+ * Call this in your +layout.server.ts or +page.server.ts to fetch the config
13
+ * bundle on the server, enabling SSR without FOOC (Flash of Original Content).
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // src/routes/+layout.server.ts
18
+ * import { loadTrafficalBundle } from '@traffical/svelte/sveltekit';
19
+ * import { TRAFFICAL_API_KEY } from '$env/static/private';
20
+ *
21
+ * export async function load({ fetch }) {
22
+ * const { bundle } = await loadTrafficalBundle({
23
+ * orgId: 'org_123',
24
+ * projectId: 'proj_456',
25
+ * env: 'production',
26
+ * apiKey: TRAFFICAL_API_KEY,
27
+ * fetch,
28
+ * });
29
+ *
30
+ * return {
31
+ * traffical: { bundle },
32
+ * };
33
+ * }
34
+ * ```
35
+ */
36
+ export declare function loadTrafficalBundle(options: LoadTrafficalBundleOptions): Promise<LoadTrafficalBundleResult>;
37
+ /**
38
+ * Resolves parameters on the server for SSR.
39
+ *
40
+ * Use this to pre-resolve specific parameters in your load function,
41
+ * enabling server-side rendering with the correct values.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // src/routes/checkout/+page.server.ts
46
+ * import { loadTrafficalBundle, resolveParamsSSR } from '@traffical/svelte/sveltekit';
47
+ *
48
+ * export async function load({ fetch, cookies }) {
49
+ * const { bundle } = await loadTrafficalBundle({ ... });
50
+ *
51
+ * // Get user context from cookies/session
52
+ * const userId = cookies.get('userId');
53
+ *
54
+ * // Pre-resolve params for this page
55
+ * const checkoutParams = resolveParamsSSR(
56
+ * bundle,
57
+ * { userId },
58
+ * {
59
+ * 'checkout.ctaText': 'Buy Now',
60
+ * 'checkout.ctaColor': '#000',
61
+ * }
62
+ * );
63
+ *
64
+ * return {
65
+ * traffical: { bundle },
66
+ * checkoutParams,
67
+ * };
68
+ * }
69
+ * ```
70
+ */
71
+ export declare function resolveParamsSSR<T extends Record<string, ParameterValue>>(bundle: ConfigBundle | null, context: Context, defaults: T): T;
72
+ export type { LoadTrafficalBundleOptions, LoadTrafficalBundleResult };
73
+ //# sourceMappingURL=sveltekit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sveltekit.d.ts","sourceRoot":"","sources":["../src/sveltekit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,KAAK,EACV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AAYpB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,yBAAyB,CAAC,CA4BpC;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,EACvE,MAAM,EAAE,YAAY,GAAG,IAAI,EAC3B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,CAAC,GACV,CAAC,CAMH;AAMD,YAAY,EAAE,0BAA0B,EAAE,yBAAyB,EAAE,CAAC"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @traffical/svelte - SvelteKit Helpers
3
+ *
4
+ * Server-side utilities for SvelteKit load functions.
5
+ * Enables SSR with pre-fetched config bundles.
6
+ */
7
+ import { resolveParameters } from "@traffical/core";
8
+ // =============================================================================
9
+ // Constants
10
+ // =============================================================================
11
+ const DEFAULT_BASE_URL = "https://sdk.traffical.io";
12
+ // =============================================================================
13
+ // Load Functions
14
+ // =============================================================================
15
+ /**
16
+ * Loads the Traffical config bundle in a SvelteKit load function.
17
+ *
18
+ * Call this in your +layout.server.ts or +page.server.ts to fetch the config
19
+ * bundle on the server, enabling SSR without FOOC (Flash of Original Content).
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // src/routes/+layout.server.ts
24
+ * import { loadTrafficalBundle } from '@traffical/svelte/sveltekit';
25
+ * import { TRAFFICAL_API_KEY } from '$env/static/private';
26
+ *
27
+ * export async function load({ fetch }) {
28
+ * const { bundle } = await loadTrafficalBundle({
29
+ * orgId: 'org_123',
30
+ * projectId: 'proj_456',
31
+ * env: 'production',
32
+ * apiKey: TRAFFICAL_API_KEY,
33
+ * fetch,
34
+ * });
35
+ *
36
+ * return {
37
+ * traffical: { bundle },
38
+ * };
39
+ * }
40
+ * ```
41
+ */
42
+ export async function loadTrafficalBundle(options) {
43
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
44
+ const url = `${baseUrl}/v1/config/${options.projectId}?env=${options.env}`;
45
+ try {
46
+ const response = await options.fetch(url, {
47
+ method: "GET",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ Authorization: `Bearer ${options.apiKey}`,
51
+ },
52
+ });
53
+ if (!response.ok) {
54
+ return {
55
+ bundle: null,
56
+ error: `HTTP ${response.status}: ${response.statusText}`,
57
+ };
58
+ }
59
+ const bundle = (await response.json());
60
+ return { bundle };
61
+ }
62
+ catch (err) {
63
+ return {
64
+ bundle: null,
65
+ error: err instanceof Error ? err.message : String(err),
66
+ };
67
+ }
68
+ }
69
+ // =============================================================================
70
+ // SSR Resolution
71
+ // =============================================================================
72
+ /**
73
+ * Resolves parameters on the server for SSR.
74
+ *
75
+ * Use this to pre-resolve specific parameters in your load function,
76
+ * enabling server-side rendering with the correct values.
77
+ *
78
+ * @example
79
+ * ```typescript
80
+ * // src/routes/checkout/+page.server.ts
81
+ * import { loadTrafficalBundle, resolveParamsSSR } from '@traffical/svelte/sveltekit';
82
+ *
83
+ * export async function load({ fetch, cookies }) {
84
+ * const { bundle } = await loadTrafficalBundle({ ... });
85
+ *
86
+ * // Get user context from cookies/session
87
+ * const userId = cookies.get('userId');
88
+ *
89
+ * // Pre-resolve params for this page
90
+ * const checkoutParams = resolveParamsSSR(
91
+ * bundle,
92
+ * { userId },
93
+ * {
94
+ * 'checkout.ctaText': 'Buy Now',
95
+ * 'checkout.ctaColor': '#000',
96
+ * }
97
+ * );
98
+ *
99
+ * return {
100
+ * traffical: { bundle },
101
+ * checkoutParams,
102
+ * };
103
+ * }
104
+ * ```
105
+ */
106
+ export function resolveParamsSSR(bundle, context, defaults) {
107
+ if (!bundle) {
108
+ return defaults;
109
+ }
110
+ return resolveParameters(bundle, context, defaults);
111
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @traffical/svelte - SvelteKit Helpers Tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=sveltekit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sveltekit.test.d.ts","sourceRoot":"","sources":["../src/sveltekit.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @traffical/svelte - SvelteKit Helpers Tests
3
+ */
4
+ import { describe, test, expect, mock } from "bun:test";
5
+ import { loadTrafficalBundle, resolveParamsSSR } from "./sveltekit.js";
6
+ // =============================================================================
7
+ // Test Fixtures
8
+ // =============================================================================
9
+ const mockBundle = {
10
+ version: new Date().toISOString(),
11
+ orgId: "org_test",
12
+ projectId: "proj_test",
13
+ env: "test",
14
+ hashing: {
15
+ unitKey: "userId",
16
+ bucketCount: 10000,
17
+ },
18
+ parameters: [
19
+ {
20
+ key: "checkout.ctaText",
21
+ type: "string",
22
+ default: "Buy Now",
23
+ layerId: "layer_1",
24
+ namespace: "checkout",
25
+ },
26
+ ],
27
+ layers: [
28
+ {
29
+ id: "layer_1",
30
+ policies: [],
31
+ },
32
+ ],
33
+ domBindings: [],
34
+ };
35
+ // =============================================================================
36
+ // loadTrafficalBundle Tests
37
+ // =============================================================================
38
+ describe("loadTrafficalBundle", () => {
39
+ test("returns bundle on successful fetch", async () => {
40
+ const mockFetch = mock(() => Promise.resolve({
41
+ ok: true,
42
+ json: () => Promise.resolve(mockBundle),
43
+ }));
44
+ const result = await loadTrafficalBundle({
45
+ orgId: "org_123",
46
+ projectId: "proj_456",
47
+ env: "production",
48
+ apiKey: "pk_test",
49
+ fetch: mockFetch,
50
+ });
51
+ expect(result.bundle).toEqual(mockBundle);
52
+ expect(result.error).toBeUndefined();
53
+ });
54
+ test("returns null bundle on HTTP error", async () => {
55
+ const mockFetch = mock(() => Promise.resolve({
56
+ ok: false,
57
+ status: 404,
58
+ statusText: "Not Found",
59
+ }));
60
+ const result = await loadTrafficalBundle({
61
+ orgId: "org_123",
62
+ projectId: "proj_456",
63
+ env: "production",
64
+ apiKey: "pk_test",
65
+ fetch: mockFetch,
66
+ });
67
+ expect(result.bundle).toBeNull();
68
+ expect(result.error).toBe("HTTP 404: Not Found");
69
+ });
70
+ test("returns null bundle on network error", async () => {
71
+ const mockFetch = mock(() => Promise.reject(new Error("Network error")));
72
+ const result = await loadTrafficalBundle({
73
+ orgId: "org_123",
74
+ projectId: "proj_456",
75
+ env: "production",
76
+ apiKey: "pk_test",
77
+ fetch: mockFetch,
78
+ });
79
+ expect(result.bundle).toBeNull();
80
+ expect(result.error).toBe("Network error");
81
+ });
82
+ test("uses correct URL format", async () => {
83
+ let capturedUrl = "";
84
+ const mockFetch = mock((url) => {
85
+ capturedUrl = url;
86
+ return Promise.resolve({
87
+ ok: true,
88
+ json: () => Promise.resolve(mockBundle),
89
+ });
90
+ });
91
+ await loadTrafficalBundle({
92
+ orgId: "org_123",
93
+ projectId: "proj_456",
94
+ env: "staging",
95
+ apiKey: "pk_test",
96
+ fetch: mockFetch,
97
+ });
98
+ expect(capturedUrl).toBe("https://sdk.traffical.io/v1/config/proj_456?env=staging");
99
+ });
100
+ test("uses custom baseUrl when provided", async () => {
101
+ let capturedUrl = "";
102
+ const mockFetch = mock((url) => {
103
+ capturedUrl = url;
104
+ return Promise.resolve({
105
+ ok: true,
106
+ json: () => Promise.resolve(mockBundle),
107
+ });
108
+ });
109
+ await loadTrafficalBundle({
110
+ orgId: "org_123",
111
+ projectId: "proj_456",
112
+ env: "production",
113
+ apiKey: "pk_test",
114
+ fetch: mockFetch,
115
+ baseUrl: "https://custom.api.com",
116
+ });
117
+ expect(capturedUrl).toBe("https://custom.api.com/v1/config/proj_456?env=production");
118
+ });
119
+ test("includes authorization header", async () => {
120
+ let capturedOptions;
121
+ const mockFetch = mock((_url, options) => {
122
+ capturedOptions = options;
123
+ return Promise.resolve({
124
+ ok: true,
125
+ json: () => Promise.resolve(mockBundle),
126
+ });
127
+ });
128
+ await loadTrafficalBundle({
129
+ orgId: "org_123",
130
+ projectId: "proj_456",
131
+ env: "production",
132
+ apiKey: "pk_secret_key",
133
+ fetch: mockFetch,
134
+ });
135
+ expect(capturedOptions?.headers).toEqual({
136
+ "Content-Type": "application/json",
137
+ Authorization: "Bearer pk_secret_key",
138
+ });
139
+ });
140
+ });
141
+ // =============================================================================
142
+ // resolveParamsSSR Tests
143
+ // =============================================================================
144
+ describe("resolveParamsSSR", () => {
145
+ test("returns defaults when bundle is null", () => {
146
+ const defaults = {
147
+ "checkout.ctaText": "Default",
148
+ };
149
+ const result = resolveParamsSSR(null, { userId: "user_123" }, defaults);
150
+ expect(result).toEqual(defaults);
151
+ });
152
+ test("resolves parameters from bundle", () => {
153
+ const defaults = {
154
+ "checkout.ctaText": "Fallback",
155
+ };
156
+ const result = resolveParamsSSR(mockBundle, { userId: "user_123" }, defaults);
157
+ expect(result["checkout.ctaText"]).toBe("Buy Now");
158
+ });
159
+ test("uses context for resolution", () => {
160
+ const defaults = {
161
+ "checkout.ctaText": "Fallback",
162
+ };
163
+ // Both should resolve to the same value since we're using defaults
164
+ const result1 = resolveParamsSSR(mockBundle, { userId: "user_1" }, defaults);
165
+ const result2 = resolveParamsSSR(mockBundle, { userId: "user_2" }, defaults);
166
+ // Without active policies, results should be the same (bundle defaults)
167
+ expect(result1["checkout.ctaText"]).toBe("Buy Now");
168
+ expect(result2["checkout.ctaText"]).toBe("Buy Now");
169
+ });
170
+ });