Skip to content

Commit 897905b

Browse files
nosnibor89robinson-mdkinyoklion
authored
feat: add initial SDK library boilerplate and basic svelte LD SDK (#632)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) (TBH `yarn run contract-tests` failed for me even in `main`) - [x] I have validated my changes against all supported platform versions **Related issues** No issue **Describe the solution you've provided** Introducing the new `@launchdarkly/svelte-client-sdk` package. Some of the details included in this PR are 1. Svelte Library Boilerplate 2. Basic Svelte SDK functionality: 2.1 `LDProvider` component 2.2 `LDFlag` component 2.3 Svelte-compatible LD instance (exposes API to work with feature flags) **Describe alternatives you've considered** I don't know what to write here. **Additional context** This is the first of a series of PRs. Some of the following PR should be about 1. Adding Documentation for @launchdarkly/svelte-client-sdk 2. Adding Example project that uses `@launchdarkly/svelte-client-sdk` --------- Co-authored-by: Robinson Marquez <[email protected]> Co-authored-by: Ryan Lamb <[email protected]>
1 parent d723e62 commit 897905b

20 files changed

+613
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"packages/sdk/react-universal",
1515
"packages/sdk/react-universal/example",
1616
"packages/sdk/vercel",
17+
"packages/sdk/svelte",
1718
"packages/sdk/akamai-base",
1819
"packages/sdk/akamai-base/example",
1920
"packages/sdk/akamai-edgekv",

packages/sdk/svelte/.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.DS_Store
2+
node_modules
3+
/build
4+
/dist
5+
/.svelte-kit
6+
/package
7+
.env
8+
.env.*
9+
!.env.example
10+
vite.config.js.timestamp-*
11+
12+
# Playwright
13+
/test-results
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { EventEmitter } from 'node:events';
2+
import { get } from 'svelte/store';
3+
import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
4+
5+
import { initialize, LDClient } from '@launchdarkly/js-client-sdk/compat';
6+
7+
import { LD } from '../../../src/lib/client/SvelteLDClient';
8+
9+
vi.mock('@launchdarkly/js-client-sdk/compat', { spy: true });
10+
11+
const clientSideID = 'test-client-side-id';
12+
const rawFlags = { 'test-flag': true, 'another-test-flag': 'flag-value' };
13+
const mockContext = { key: 'user1' };
14+
15+
// used to mock ready and change events on the LDClient
16+
const mockLDEventEmitter = new EventEmitter();
17+
18+
const mockLDClient = {
19+
on: (e: string, cb: () => void) => mockLDEventEmitter.on(e, cb),
20+
off: vi.fn(),
21+
allFlags: vi.fn().mockReturnValue(rawFlags),
22+
variation: vi.fn((_, defaultValue) => defaultValue),
23+
identify: vi.fn(),
24+
};
25+
26+
describe('launchDarkly', () => {
27+
describe('createLD', () => {
28+
it('should create a LaunchDarkly instance with correct properties', () => {
29+
const ld = LD;
30+
expect(typeof ld).toBe('object');
31+
expect(ld).toHaveProperty('identify');
32+
expect(ld).toHaveProperty('flags');
33+
expect(ld).toHaveProperty('initialize');
34+
expect(ld).toHaveProperty('initializing');
35+
expect(ld).toHaveProperty('watch');
36+
expect(ld).toHaveProperty('useFlag');
37+
});
38+
39+
describe('initialize', async () => {
40+
const ld = LD;
41+
42+
beforeEach(() => {
43+
// mocks the initialize function to return the mockLDClient
44+
(initialize as Mock<typeof initialize>).mockReturnValue(
45+
mockLDClient as unknown as LDClient,
46+
);
47+
});
48+
49+
afterEach(() => {
50+
vi.clearAllMocks();
51+
mockLDEventEmitter.removeAllListeners();
52+
});
53+
54+
it('should throw an error if the client is not initialized', async () => {
55+
const flagKey = 'test-flag';
56+
const user = { key: 'user1' };
57+
58+
expect(() => ld.useFlag(flagKey, true)).toThrow('LaunchDarkly client not initialized');
59+
await expect(() => ld.identify(user)).rejects.toThrow(
60+
'LaunchDarkly client not initialized',
61+
);
62+
});
63+
64+
it('should set the loading status to false when the client is ready', async () => {
65+
const { initializing } = ld;
66+
ld.initialize(clientSideID, mockContext);
67+
68+
expect(get(initializing)).toBe(true); // should be true before the ready event is emitted
69+
mockLDEventEmitter.emit('ready');
70+
71+
expect(get(initializing)).toBe(false);
72+
});
73+
74+
it('should initialize the LaunchDarkly SDK instance', () => {
75+
ld.initialize(clientSideID, mockContext);
76+
77+
expect(initialize).toHaveBeenCalledWith('test-client-side-id', mockContext);
78+
});
79+
80+
it('should register function that gets flag values when client is ready', () => {
81+
const newFlags = { ...rawFlags, 'new-flag': true };
82+
const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(newFlags);
83+
84+
ld.initialize(clientSideID, mockContext);
85+
mockLDEventEmitter.emit('ready');
86+
87+
expect(allFlagsSpy).toHaveBeenCalledOnce();
88+
expect(allFlagsSpy).toHaveReturnedWith(newFlags);
89+
});
90+
91+
it('should register function that gets flag values when flags changed', () => {
92+
const changedFlags = { ...rawFlags, 'changed-flag': true };
93+
const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(changedFlags);
94+
95+
ld.initialize(clientSideID, mockContext);
96+
mockLDEventEmitter.emit('change');
97+
98+
expect(allFlagsSpy).toHaveBeenCalledOnce();
99+
expect(allFlagsSpy).toHaveReturnedWith(changedFlags);
100+
});
101+
});
102+
103+
describe('watch function', () => {
104+
const ld = LD;
105+
106+
beforeEach(() => {
107+
// mocks the initialize function to return the mockLDClient
108+
(initialize as Mock<typeof initialize>).mockReturnValue(
109+
mockLDClient as unknown as LDClient,
110+
);
111+
});
112+
113+
afterEach(() => {
114+
vi.clearAllMocks();
115+
mockLDEventEmitter.removeAllListeners();
116+
});
117+
118+
it('should return a derived store that reflects the value of the specified flag', () => {
119+
const flagKey = 'test-flag';
120+
ld.initialize(clientSideID, mockContext);
121+
122+
const flagStore = ld.watch(flagKey);
123+
124+
expect(get(flagStore)).toBe(true);
125+
});
126+
127+
it('should update the flag store when the flag value changes', () => {
128+
const booleanFlagKey = 'test-flag';
129+
const stringFlagKey = 'another-test-flag';
130+
ld.initialize(clientSideID, mockContext);
131+
const flagStore = ld.watch(booleanFlagKey);
132+
const flagStore2 = ld.watch(stringFlagKey);
133+
134+
// emit ready event to set initial flag values
135+
mockLDEventEmitter.emit('ready');
136+
137+
// 'test-flag' initial value is true according to `rawFlags`
138+
expect(get(flagStore)).toBe(true);
139+
// 'another-test-flag' intial value is 'flag-value' according to `rawFlags`
140+
expect(get(flagStore2)).toBe('flag-value');
141+
142+
mockLDClient.allFlags.mockReturnValue({
143+
...rawFlags,
144+
'test-flag': false,
145+
'another-test-flag': 'new-flag-value',
146+
});
147+
148+
// dispatch a change event on ldClient
149+
mockLDEventEmitter.emit('change');
150+
151+
expect(get(flagStore)).toBe(false);
152+
expect(get(flagStore2)).toBe('new-flag-value');
153+
});
154+
155+
it('should return undefined if the flag is not found', () => {
156+
const flagKey = 'non-existent-flag';
157+
ld.initialize(clientSideID, mockContext);
158+
159+
const flagStore = ld.watch(flagKey);
160+
161+
expect(get(flagStore)).toBeUndefined();
162+
});
163+
});
164+
165+
describe('useFlag function', () => {
166+
const ld = LD;
167+
168+
beforeEach(() => {
169+
// mocks the initialize function to return the mockLDClient
170+
(initialize as Mock<typeof initialize>).mockReturnValue(
171+
mockLDClient as unknown as LDClient,
172+
);
173+
});
174+
175+
afterEach(() => {
176+
vi.clearAllMocks();
177+
mockLDEventEmitter.removeAllListeners();
178+
});
179+
180+
it('should return flag value', () => {
181+
mockLDClient.variation.mockReturnValue(true);
182+
const flagKey = 'test-flag';
183+
ld.initialize(clientSideID, mockContext);
184+
185+
expect(ld.useFlag(flagKey, false)).toBe(true);
186+
expect(mockLDClient.variation).toHaveBeenCalledWith(flagKey, false);
187+
});
188+
});
189+
190+
describe('identify function', () => {
191+
const ld = LD;
192+
193+
beforeEach(() => {
194+
// mocks the initialize function to return the mockLDClient
195+
(initialize as Mock<typeof initialize>).mockReturnValue(
196+
mockLDClient as unknown as LDClient,
197+
);
198+
});
199+
200+
afterEach(() => {
201+
vi.clearAllMocks();
202+
mockLDEventEmitter.removeAllListeners();
203+
});
204+
205+
it('should call the identify method on the LaunchDarkly client', () => {
206+
const user = { key: 'user1' };
207+
ld.initialize(clientSideID, user);
208+
209+
ld.identify(user);
210+
211+
expect(mockLDClient.identify).toHaveBeenCalledWith(user);
212+
});
213+
});
214+
});
215+
});

packages/sdk/svelte/package.json

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{
2+
"name": "@launchdarkly/svelte-client-sdk",
3+
"version": "0.1.0",
4+
"description": "Svelte LaunchDarkly SDK",
5+
"homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/svelte",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/launchdarkly/js-core.git"
9+
},
10+
"license": "Apache-2.0",
11+
"packageManager": "[email protected]",
12+
"keywords": [
13+
"launchdarkly",
14+
"svelte"
15+
],
16+
"type": "module",
17+
"svelte": "./dist/index.js",
18+
"types": "./dist/index.d.ts",
19+
"exports": {
20+
".": {
21+
"types": "./dist/index.d.ts",
22+
"svelte": "./dist/index.js",
23+
"default": "./dist/index.js"
24+
}
25+
},
26+
"files": [
27+
"dist",
28+
"!dist/**/*.test.*",
29+
"!dist/**/*.spec.*"
30+
],
31+
"scripts": {
32+
"clean": "rimraf dist",
33+
"dev": "vite dev",
34+
"build": "vite build && npm run package",
35+
"preview": "vite preview",
36+
"package": "svelte-kit sync && svelte-package && publint",
37+
"prepublishOnly": "npm run package",
38+
"lint": "eslint . --ext .ts,.tsx",
39+
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore",
40+
"check": "yarn prettier && yarn lint && yarn build && yarn test",
41+
"test": "playwright test",
42+
"test:unit": "vitest",
43+
"test:unit-ui": "vitest --ui",
44+
"test:unit-coverage": "vitest --coverage"
45+
},
46+
"peerDependencies": {
47+
"@launchdarkly/js-client-sdk": "workspace:^",
48+
"svelte": "^4.0.0"
49+
},
50+
"dependencies": {
51+
"@launchdarkly/js-client-sdk": "workspace:^",
52+
"esm-env": "^1.0.0"
53+
},
54+
"devDependencies": {
55+
"@playwright/test": "^1.28.1",
56+
"@sveltejs/adapter-auto": "^3.0.0",
57+
"@sveltejs/kit": "^2.0.0",
58+
"@sveltejs/package": "^2.0.0",
59+
"@sveltejs/vite-plugin-svelte": "^5.0.1",
60+
"@testing-library/svelte": "^5.2.0",
61+
"@types/jest": "^29.5.11",
62+
"@typescript-eslint/eslint-plugin": "^6.20.0",
63+
"@typescript-eslint/parser": "^6.20.0",
64+
"@vitest/coverage-v8": "^2.1.8",
65+
"@vitest/ui": "^2.1.8",
66+
"eslint": "^8.45.0",
67+
"eslint-config-airbnb-base": "^15.0.0",
68+
"eslint-config-airbnb-typescript": "^17.1.0",
69+
"eslint-config-prettier": "^8.8.0",
70+
"eslint-plugin-import": "^2.27.5",
71+
"eslint-plugin-jest": "^27.6.3",
72+
"eslint-plugin-prettier": "^5.0.0",
73+
"eslint-plugin-svelte": "^2.35.1",
74+
"jsdom": "^24.0.0",
75+
"launchdarkly-js-test-helpers": "^2.2.0",
76+
"prettier": "^3.0.0",
77+
"prettier-plugin-svelte": "^3.1.2",
78+
"publint": "^0.1.9",
79+
"rimraf": "^5.0.5",
80+
"svelte": "^5.4.0",
81+
"svelte-check": "^3.6.0",
82+
"ts-jest": "^29.1.1",
83+
"ts-node": "^10.9.2",
84+
"typedoc": "0.25.0",
85+
"typescript": "5.1.6",
86+
"vite": "^6.0.2",
87+
"vitest": "^2.1.8"
88+
}
89+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
3+
const config: PlaywrightTestConfig = {
4+
webServer: {
5+
command: 'npm run build && npm run preview',
6+
port: 4173
7+
},
8+
testDir: 'tests',
9+
testMatch: /(.+\.)?(test|spec)\.[jt]s/
10+
};
11+
12+
export default config;

packages/sdk/svelte/src/app.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// See https://kit.svelte.dev/docs/types#app
2+
// for information about these interfaces
3+
declare global {
4+
namespace App {
5+
// interface Error {}
6+
// interface Locals {}
7+
// interface PageData {}
8+
// interface PageState {}
9+
// interface Platform {}
10+
}
11+
}
12+
13+
export {};

packages/sdk/svelte/src/app.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
%sveltekit.head%
7+
</head>
8+
<body data-sveltekit-preload-data="hover">
9+
<div>%sveltekit.body%</div>
10+
</body>
11+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
import { LD, type LDFlagValue } from './client/SvelteLDClient.js';
3+
4+
export let flag: string;
5+
export let matches: LDFlagValue = true;
6+
7+
$: flagValue = LD.watch(flag);
8+
</script>
9+
10+
{#if $flagValue === matches}
11+
<slot name="true" />
12+
{:else}
13+
<slot name="false" />
14+
{/if}

0 commit comments

Comments
 (0)