From 26f564ee80379ce8c882dd5b91c5b83fcc0062b4 Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Wed, 23 Oct 2024 17:04:58 -0300 Subject: [PATCH 01/12] add initial SDK library boilerplate and basic svelte LD SDK --- package.json | 1 + packages/sdk/svelte/.gitignore | 13 + packages/sdk/svelte/package.json | 89 +++++++ packages/sdk/svelte/playwright.config.ts | 12 + packages/sdk/svelte/src/app.d.ts | 13 + packages/sdk/svelte/src/app.html | 11 + packages/sdk/svelte/src/lib/LDFlag.svelte | 14 + .../svelte/src/lib/client/SvelteLDClient.ts | 105 ++++++++ .../client/__tests__/SvelteLDClient.test.ts | 244 ++++++++++++++++++ packages/sdk/svelte/src/lib/client/index.ts | 1 + packages/sdk/svelte/src/lib/index.ts | 6 + .../svelte/src/lib/provider/LDProvider.svelte | 19 ++ packages/sdk/svelte/svelte.config.js | 18 ++ packages/sdk/svelte/tsconfig.eslint.json | 5 + packages/sdk/svelte/tsconfig.json | 15 ++ packages/sdk/svelte/tsconfig.ref.json | 7 + packages/sdk/svelte/tsconfig.test.json | 13 + packages/sdk/svelte/typedoc.json | 5 + packages/sdk/svelte/vite.config.ts | 17 ++ tsconfig.json | 3 + 20 files changed, 611 insertions(+) create mode 100644 packages/sdk/svelte/.gitignore create mode 100644 packages/sdk/svelte/package.json create mode 100644 packages/sdk/svelte/playwright.config.ts create mode 100644 packages/sdk/svelte/src/app.d.ts create mode 100644 packages/sdk/svelte/src/app.html create mode 100644 packages/sdk/svelte/src/lib/LDFlag.svelte create mode 100644 packages/sdk/svelte/src/lib/client/SvelteLDClient.ts create mode 100644 packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts create mode 100644 packages/sdk/svelte/src/lib/client/index.ts create mode 100644 packages/sdk/svelte/src/lib/index.ts create mode 100644 packages/sdk/svelte/src/lib/provider/LDProvider.svelte create mode 100644 packages/sdk/svelte/svelte.config.js create mode 100644 packages/sdk/svelte/tsconfig.eslint.json create mode 100644 packages/sdk/svelte/tsconfig.json create mode 100644 packages/sdk/svelte/tsconfig.ref.json create mode 100644 packages/sdk/svelte/tsconfig.test.json create mode 100644 packages/sdk/svelte/typedoc.json create mode 100644 packages/sdk/svelte/vite.config.ts diff --git a/package.json b/package.json index 2778c9e9d2..f3ed775d8f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "packages/sdk/react-universal", "packages/sdk/react-universal/example", "packages/sdk/vercel", + "packages/sdk/svelte", "packages/sdk/akamai-base", "packages/sdk/akamai-base/example", "packages/sdk/akamai-edgekv", diff --git a/packages/sdk/svelte/.gitignore b/packages/sdk/svelte/.gitignore new file mode 100644 index 0000000000..5396c65c3c --- /dev/null +++ b/packages/sdk/svelte/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/dist +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* + +# Playwright +/test-results \ No newline at end of file diff --git a/packages/sdk/svelte/package.json b/packages/sdk/svelte/package.json new file mode 100644 index 0000000000..86bd18b289 --- /dev/null +++ b/packages/sdk/svelte/package.json @@ -0,0 +1,89 @@ +{ + "name": "@launchdarkly/svelte-client-sdk", + "version": "1.0.0", + "description": "Svelte LaunchDarkly SDK", + "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/svelte", + "repository": { + "type": "git", + "url": "https://github.com/launchdarkly/js-core.git" + }, + "license": "Apache-2.0", + "packageManager": "yarn@3.4.1", + "keywords": [ + "launchdarkly", + "svelte" + ], + "type": "module", + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "scripts": { + "clean": "rimraf dist", + "dev": "vite dev", + "build": "vite build && npm run package", + "preview": "vite preview", + "package": "svelte-kit sync && svelte-package && publint", + "prepublishOnly": "npm run package", + "lint": "eslint . --ext .ts,.tsx", + "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", + "check": "yarn prettier && yarn lint && yarn build && yarn test", + "test": "playwright test", + "test:unit": "vitest", + "test:unit-ui": "vitest --ui" + }, + "peerDependencies": { + "@launchdarkly/js-client-sdk-common": "^1.1.4", + "@launchdarkly/node-server-sdk": "^9.4.6", + "launchdarkly-js-client-sdk": "^3.4.0", + "svelte": "^4.0.0" + }, + "dependencies": { + "@launchdarkly/js-client-sdk-common": "1.1.4", + "esm-env": "^1.0.0" + }, + "devDependencies": { + "@playwright/test": "^1.28.1", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/package": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@testing-library/svelte": "^5.2.0", + "@types/jest": "^29.5.11", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "@vitest/ui": "^1.6.0", + "eslint": "^8.45.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-svelte": "^2.35.1", + "jsdom": "^24.0.0", + "launchdarkly-js-test-helpers": "^2.2.0", + "prettier": "^3.0.0", + "prettier-plugin-svelte": "^3.1.2", + "publint": "^0.1.9", + "rimraf": "^5.0.5", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typedoc": "0.25.0", + "typescript": "5.1.6", + "vite": "^5.2.6", + "vitest": "^1.6.0" + } +} diff --git a/packages/sdk/svelte/playwright.config.ts b/packages/sdk/svelte/playwright.config.ts new file mode 100644 index 0000000000..1c5d7a1fd3 --- /dev/null +++ b/packages/sdk/svelte/playwright.config.ts @@ -0,0 +1,12 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/ +}; + +export default config; diff --git a/packages/sdk/svelte/src/app.d.ts b/packages/sdk/svelte/src/app.d.ts new file mode 100644 index 0000000000..ede601ab93 --- /dev/null +++ b/packages/sdk/svelte/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/sdk/svelte/src/app.html b/packages/sdk/svelte/src/app.html new file mode 100644 index 0000000000..f90b0a64e4 --- /dev/null +++ b/packages/sdk/svelte/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/sdk/svelte/src/lib/LDFlag.svelte b/packages/sdk/svelte/src/lib/LDFlag.svelte new file mode 100644 index 0000000000..230e78908d --- /dev/null +++ b/packages/sdk/svelte/src/lib/LDFlag.svelte @@ -0,0 +1,14 @@ + + + {#if $flagValue === matches} + + {:else} + + {/if} diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts new file mode 100644 index 0000000000..3ec4515bde --- /dev/null +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -0,0 +1,105 @@ +import { initialize } from 'launchdarkly-js-client-sdk'; +import type { + LDClient, + LDFlagSet, + LDFlagValue, + LDContext as NodeLDContext, +} from 'launchdarkly-js-client-sdk'; +import { derived, get, type Readable, readonly, writable, type Writable } from 'svelte/store'; + +/** Client ID for LaunchDarkly */ +export type LDClientID = string; + +/** Context for LaunchDarkly */ +export type LDContext = NodeLDContext; + +/** Value of LaunchDarkly flags */ +export type LDFlagsValue = LDFlagValue; + +/** Flags for LaunchDarkly */ +export type LDFlags = LDFlagSet; + +/** + * Checks if the LaunchDarkly client is initialized. + * @param {LDClient | undefined} client - The LaunchDarkly client. + * @throws {Error} If the client is not initialized. + */ +function isClientInitialized(client: LDClient | undefined): asserts client is LDClient { + if (!client) { + throw new Error('LaunchDarkly client not initialized'); + } +} + +/** + * Creates a LaunchDarkly instance. + * @returns {Object} The LaunchDarkly instance object. + */ +function createLD() { + let jsSdk: LDClient | undefined; + const loading = writable(true); + const flagsWritable = writable({}); + + /** + * Initializes the LaunchDarkly client. + * @param {LDClientID} clientId - The client ID. + * @param {LDContext} context - The context. + * @returns {Writable} An object with the initialization status store. + */ + function LDInitialize(clientId: LDClientID, context: LDContext) { + jsSdk = initialize(clientId, context); + + jsSdk.waitUntilReady().then(() => { + loading.set(false); + flagsWritable.set(jsSdk!.allFlags()); + }); + + jsSdk.on('change', () => { + flagsWritable.set(jsSdk!.allFlags()); + }); + + return { + initializing: loading, + }; + } + + /** + * Identifies the user context. + * @param {LDContext} context - The user context. + * @returns {Promise} A promise that resolves when the user is identified. + */ + async function identify(context: LDContext) { + isClientInitialized(jsSdk); + return jsSdk.identify(context); + } + + /** + * Watches a flag for changes. + * @param {string} flagKey - The key of the flag to watch. + * @returns {Readable} A readable store of the flag value. + */ + const watch = (flagKey: string): Readable => + derived, LDFlagsValue>(flagsWritable, ($flags) => $flags[flagKey]); + + /** + * Checks if a flag is on. + * @param {string} flagKey - The key of the flag to check. + * @returns {boolean} True if the flag is on, false otherwise. + */ + const isOn = (flagKey: string): boolean => { + isClientInitialized(jsSdk); + const currentFlags = get(flagsWritable); + return !!currentFlags[flagKey]; + }; + + return { + identify, + flags: readonly(flagsWritable), + initialize: LDInitialize, + initializing: readonly(loading), + watch, + isOn, + }; +} + +/** The LaunchDarkly instance */ +export const LD = createLD(); diff --git a/packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts b/packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts new file mode 100644 index 0000000000..7624bb5fe9 --- /dev/null +++ b/packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts @@ -0,0 +1,244 @@ +import * as LDClient from 'launchdarkly-js-client-sdk'; +import { get } from 'svelte/store'; +import { afterAll, afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; + +import { LD } from '../SvelteLDClient.js'; + +vi.mock('launchdarkly-js-client-sdk', async (importActual) => { + const actual = (await importActual()) as typeof LDClient; + return { + ...actual, + initialize: vi.fn(), + }; +}); + +const clientSideID = 'test-client-side-id'; +const rawFlags = { 'test-flag': true, 'another-test-flag': true }; +const mockLDClient = { + on: vi.fn((e: string, cb: () => void) => { + cb(); + }), + off: vi.fn(), + allFlags: vi.fn().mockReturnValue({}), + variation: vi.fn(), + waitForInitialization: vi.fn(), + waitUntilReady: vi.fn().mockResolvedValue(undefined), + identify: vi.fn(), +}; +const mockInitialize = LDClient.initialize as Mock; +const mockAllFlags = mockLDClient.allFlags as Mock; + +describe('launchDarkly', () => { + describe('createLD', () => { + it('should create a LaunchDarkly instance with correct properties', () => { + const ld = LD; + expect(typeof ld).toBe('object'); + expect(ld).toHaveProperty('identify'); + expect(ld).toHaveProperty('flags'); + expect(ld).toHaveProperty('initialize'); + expect(ld).toHaveProperty('initializing'); + expect(ld).toHaveProperty('watch'); + expect(ld).toHaveProperty('isOn'); + }); + + describe('initialize', async () => { + let ld = LD; + beforeEach(() => { + mockInitialize.mockImplementation(() => mockLDClient); + mockAllFlags.mockImplementation(() => rawFlags); + }); + + afterEach(() => { + mockInitialize.mockClear(); + mockAllFlags.mockClear(); + }); + + afterAll(() => { + vi.clearAllMocks(); + }); + + it('should throw an error if the client is not initialized', async () => { + ld = LD; + expect(() => ld.isOn('test-flag')).toThrow('LaunchDarkly client not initialized'); + await expect(() => ld.identify({ key: 'user1' })).rejects.toThrow( + 'LaunchDarkly client not initialized', + ); + }); + + it('should set the loading status to false when the client is ready', async () => { + const { initializing } = ld; + ld.initialize('clientId', { key: 'user1' }); + + // wait for next tick + await new Promise((r) => { + setTimeout(r); + }); + + const initializingValue = get(initializing); + expect(initializingValue).toBe(false); + }); + it('should initialize the LaunchDarkly SDK instance', () => { + const initializeSpy = vi.spyOn(LDClient, 'initialize'); + + ld.initialize('clientId', { key: 'user1' }); + expect(initializeSpy).toHaveBeenCalledWith('clientId', { key: 'user1' }); + }); + + it('should call waitUntilReady when initializing', () => { + const waitUntilReadySpy = vi.spyOn(mockLDClient, 'waitUntilReady'); + + ld.initialize('clientId', { key: 'user1' }); + + expect(waitUntilReadySpy).toHaveBeenCalled(); + }); + + it('should register an event listener for the "change" event', () => { + const onSpy = vi.spyOn(mockLDClient, 'on'); + + ld.initialize('clientId ', { key: 'user1' }); + + expect(onSpy).toHaveBeenCalled(); + expect(onSpy).toHaveBeenCalledWith('change', expect.any(Function)); + }); + + it('should set flags when the client is ready', () => { + const flagSubscriber = vi.fn(); + ld.initialize('clientId', { key: 'user1' }); + + const subscribeSpy = vi.spyOn(ld.flags, 'subscribe'); + ld.flags.subscribe(flagSubscriber); + + expect(subscribeSpy).toBeDefined(); + expect(flagSubscriber).toHaveBeenCalledTimes(1); + expect(flagSubscriber).toHaveBeenCalledWith(rawFlags); + }); + }); + describe('watch function', () => { + const ld = LD; + beforeEach(() => { + mockInitialize.mockImplementation(() => mockLDClient); + mockAllFlags.mockImplementation(() => rawFlags); + }); + + it('should return a derived store that reflects the value of the specified flag', () => { + const flagKey = 'test-flag'; + ld.initialize(clientSideID, { key: 'user1' }); + + const flagStore = ld.watch(flagKey); + + expect(get(flagStore)).toBe(true); + }); + + it('should update the flag store when the flag value changes', () => { + const flagKey = 'test-flag'; + ld.initialize(clientSideID, { key: 'user1' }); + + const flagStore = ld.watch(flagKey); + + expect(get(flagStore)).toBe(true); + + mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); + + // dispatch a change event on ldClient + const changeCallback = mockLDClient.on.mock.calls[0][1]; + changeCallback(); + + expect(get(flagStore)).toBe(false); + }); + + it('should return undefined if the flag is not found', () => { + const flagKey = 'non-existent-flag'; + ld.initialize(clientSideID, { key: 'user1' }); + + const flagStore = ld.watch(flagKey); + + expect(get(flagStore)).toBeUndefined(); + }); + }); + + describe('isOn function', () => { + const ld = LD; + beforeEach(() => { + mockInitialize.mockImplementation(() => mockLDClient); + mockAllFlags.mockImplementation(() => rawFlags); + }); + + it('should return true if the flag is on', () => { + const flagKey = 'test-flag'; + ld.initialize(clientSideID, { key: 'user1' }); + + expect(ld.isOn(flagKey)).toBe(true); + }); + + it('should return false if the flag is off', () => { + const flagKey = 'test-flag'; + ld.initialize(clientSideID, { key: 'user1' }); + + mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); + + // dispatch a change event on ldClient + const changeCallback = mockLDClient.on.mock.calls[0][1]; + changeCallback(); + + expect(ld.isOn(flagKey)).toBe(false); + }); + + it('should return false if the flag is not found', () => { + const flagKey = 'non-existent-flag'; + ld.initialize(clientSideID, { key: 'user1' }); + + expect(ld.isOn(flagKey)).toBe(false); + }); + }); + + describe('identify function', () => { + const ld = LD; + beforeEach(() => { + mockInitialize.mockImplementation(() => mockLDClient); + mockAllFlags.mockImplementation(() => rawFlags); + }); + + it('should call the identify method on the LaunchDarkly client', () => { + const user = { key: 'user1' }; + ld.initialize(clientSideID, user); + + ld.identify(user); + + expect(mockLDClient.identify).toHaveBeenCalledWith(user); + }); + }); + + describe('flags store', () => { + const ld = LD; + beforeEach(() => { + mockInitialize.mockImplementation(() => mockLDClient); + mockAllFlags.mockImplementation(() => rawFlags); + }); + + it('should return a readonly store of the flags', () => { + ld.initialize(clientSideID, { key: 'user1' }); + + const { flags } = ld; + + expect(get(flags)).toEqual(rawFlags); + }); + + it('should update the flags store when the flags change', () => { + ld.initialize(clientSideID, { key: 'user1' }); + + const { flags } = ld; + + expect(get(flags)).toEqual(rawFlags); + + const newFlags = { 'test-flag': false, 'another-test-flag': true }; + mockAllFlags.mockReturnValue(newFlags); + + // dispatch a change event on ldClient + const changeCallback = mockLDClient.on.mock.calls[0][1]; + changeCallback(); + + expect(get(flags)).toEqual(newFlags); + }); + }); + }); +}); diff --git a/packages/sdk/svelte/src/lib/client/index.ts b/packages/sdk/svelte/src/lib/client/index.ts new file mode 100644 index 0000000000..64d6e6118a --- /dev/null +++ b/packages/sdk/svelte/src/lib/client/index.ts @@ -0,0 +1 @@ +export * from './SvelteLDClient'; diff --git a/packages/sdk/svelte/src/lib/index.ts b/packages/sdk/svelte/src/lib/index.ts new file mode 100644 index 0000000000..32c2c870bb --- /dev/null +++ b/packages/sdk/svelte/src/lib/index.ts @@ -0,0 +1,6 @@ +// Reexport your entry components here +export * as LDClient from './client/SvelteLDClient.js'; + +// Export Components +export { default as LDProvider } from './provider/LDProvider.svelte'; +export { default as LDFlag } from './LDFlag.svelte'; diff --git a/packages/sdk/svelte/src/lib/provider/LDProvider.svelte b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte new file mode 100644 index 0000000000..d5a53be7fe --- /dev/null +++ b/packages/sdk/svelte/src/lib/provider/LDProvider.svelte @@ -0,0 +1,19 @@ + + +{#if $$slots.initializing && $initializing} + Loading flags (default loading slot value)... +{:else} + +{/if} diff --git a/packages/sdk/svelte/svelte.config.js b/packages/sdk/svelte/svelte.config.js new file mode 100644 index 0000000000..734094d0b6 --- /dev/null +++ b/packages/sdk/svelte/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/packages/sdk/svelte/tsconfig.eslint.json b/packages/sdk/svelte/tsconfig.eslint.json new file mode 100644 index 0000000000..8241f86c36 --- /dev/null +++ b/packages/sdk/svelte/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["/**/*.ts", "/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/sdk/svelte/tsconfig.json b/packages/sdk/svelte/tsconfig.json new file mode 100644 index 0000000000..8ed3dd7f25 --- /dev/null +++ b/packages/sdk/svelte/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/packages/sdk/svelte/tsconfig.ref.json b/packages/sdk/svelte/tsconfig.ref.json new file mode 100644 index 0000000000..34a1cb607a --- /dev/null +++ b/packages/sdk/svelte/tsconfig.ref.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "package.json"], + "compilerOptions": { + "composite": true + } +} diff --git a/packages/sdk/svelte/tsconfig.test.json b/packages/sdk/svelte/tsconfig.test.json new file mode 100644 index 0000000000..8d49b842cf --- /dev/null +++ b/packages/sdk/svelte/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "lib": ["es6", "dom"], + "module": "ES6", + "moduleResolution": "node", + "resolveJsonModule": true, + "rootDir": ".", + "strict": true, + "types": ["jest", "node"] + }, + "exclude": ["dist", "node_modules", "__tests__", "example"] +} diff --git a/packages/sdk/svelte/typedoc.json b/packages/sdk/svelte/typedoc.json new file mode 100644 index 0000000000..7ac616b544 --- /dev/null +++ b/packages/sdk/svelte/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "out": "docs" +} diff --git a/packages/sdk/svelte/vite.config.ts b/packages/sdk/svelte/vite.config.ts new file mode 100644 index 0000000000..0bc6e5ea6a --- /dev/null +++ b/packages/sdk/svelte/vite.config.ts @@ -0,0 +1,17 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import path from 'path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [sveltekit()], + resolve: { + alias: { + lib: path.resolve(__dirname, 'src/lib'), + }, + }, + test: { + include: ['src/**/*.{test,spec}.{js,ts,svelte}'], + globals: true, + environment: 'jsdom', + }, +}); diff --git a/tsconfig.json b/tsconfig.json index e7ffa9fe3b..6ce908909a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,6 +40,9 @@ { "path": "./packages/sdk/akamai-base/tsconfig.ref.json" }, + { + "path": "./packages/sdk/svelte/tsconfig.ref.json" + }, { "path": "./packages/store/node-server-sdk-redis/tsconfig.ref.json" }, From 22a3378cbd95cdb4490bf47b40b52e93f5da6f8e Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Wed, 23 Oct 2024 17:37:26 -0300 Subject: [PATCH 02/12] Refactor test file paths and update Vite config --- .../__tests__ => __tests__/lib/client}/SvelteLDClient.test.ts | 2 +- packages/sdk/svelte/vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/sdk/svelte/{src/lib/client/__tests__ => __tests__/lib/client}/SvelteLDClient.test.ts (99%) diff --git a/packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts similarity index 99% rename from packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts rename to packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index 7624bb5fe9..ac821617ab 100644 --- a/packages/sdk/svelte/src/lib/client/__tests__/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -2,7 +2,7 @@ import * as LDClient from 'launchdarkly-js-client-sdk'; import { get } from 'svelte/store'; import { afterAll, afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; -import { LD } from '../SvelteLDClient.js'; +import { LD } from '../../../src/lib/client/SvelteLDClient'; vi.mock('launchdarkly-js-client-sdk', async (importActual) => { const actual = (await importActual()) as typeof LDClient; diff --git a/packages/sdk/svelte/vite.config.ts b/packages/sdk/svelte/vite.config.ts index 0bc6e5ea6a..eef9c1da2c 100644 --- a/packages/sdk/svelte/vite.config.ts +++ b/packages/sdk/svelte/vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ }, }, test: { - include: ['src/**/*.{test,spec}.{js,ts,svelte}'], + include: ['__tests__/**/*.{test,spec}.{js,ts,svelte}'], globals: true, environment: 'jsdom', }, From 147f67b04a4203d35f2ee3c15ef9b9994b877c84 Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Wed, 6 Nov 2024 18:43:53 -0300 Subject: [PATCH 03/12] intial refactor to use new "@launchdarkly/js-client-sdk" --- .../lib/client/SvelteLDClient.test.ts | 269 +++++++++--------- packages/sdk/svelte/package.json | 9 +- .../svelte/src/lib/client/SvelteLDClient.ts | 38 ++- 3 files changed, 159 insertions(+), 157 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index ac821617ab..b0f9279547 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -1,32 +1,26 @@ -import * as LDClient from 'launchdarkly-js-client-sdk'; +import { EventEmitter } from 'node:events'; import { get } from 'svelte/store'; -import { afterAll, afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { afterAll, afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; + +import { initialize, LDClient } from '@launchdarkly/js-client-sdk'; import { LD } from '../../../src/lib/client/SvelteLDClient'; -vi.mock('launchdarkly-js-client-sdk', async (importActual) => { - const actual = (await importActual()) as typeof LDClient; - return { - ...actual, - initialize: vi.fn(), - }; -}); +vi.mock('@launchdarkly/js-client-sdk', { spy: true }); const clientSideID = 'test-client-side-id'; -const rawFlags = { 'test-flag': true, 'another-test-flag': true }; +const rawFlags = { 'test-flag': true, 'another-test-flag': 'flag-value' }; + +// used to mock ready and change events on the LDClient +const mockLDEventEmitter = new EventEmitter(); + const mockLDClient = { - on: vi.fn((e: string, cb: () => void) => { - cb(); - }), + on: (e: string, cb: () => void) => mockLDEventEmitter.on(e, cb), off: vi.fn(), - allFlags: vi.fn().mockReturnValue({}), + allFlags: vi.fn().mockReturnValue(rawFlags), variation: vi.fn(), - waitForInitialization: vi.fn(), - waitUntilReady: vi.fn().mockResolvedValue(undefined), identify: vi.fn(), }; -const mockInitialize = LDClient.initialize as Mock; -const mockAllFlags = mockLDClient.allFlags as Mock; describe('launchDarkly', () => { describe('createLD', () => { @@ -42,87 +36,87 @@ describe('launchDarkly', () => { }); describe('initialize', async () => { - let ld = LD; + const ld = LD; + beforeEach(() => { - mockInitialize.mockImplementation(() => mockLDClient); - mockAllFlags.mockImplementation(() => rawFlags); + // mocks the initialize function to return the mockLDClient + (initialize as Mock).mockReturnValue( + mockLDClient as unknown as LDClient, + ); }); afterEach(() => { - mockInitialize.mockClear(); - mockAllFlags.mockClear(); - }); - - afterAll(() => { vi.clearAllMocks(); + mockLDEventEmitter.removeAllListeners(); }); it('should throw an error if the client is not initialized', async () => { - ld = LD; - expect(() => ld.isOn('test-flag')).toThrow('LaunchDarkly client not initialized'); - await expect(() => ld.identify({ key: 'user1' })).rejects.toThrow( + const flagKey = 'test-flag'; + const user = { key: 'user1' }; + + expect(() => ld.isOn(flagKey)).toThrow('LaunchDarkly client not initialized'); + await expect(() => ld.identify(user)).rejects.toThrow( 'LaunchDarkly client not initialized', ); }); it('should set the loading status to false when the client is ready', async () => { const { initializing } = ld; - ld.initialize('clientId', { key: 'user1' }); + ld.initialize('clientId'); - // wait for next tick - await new Promise((r) => { - setTimeout(r); - }); + expect(get(initializing)).toBe(true); // should be true before the ready event is emitted + mockLDEventEmitter.emit('ready'); - const initializingValue = get(initializing); - expect(initializingValue).toBe(false); + expect(get(initializing)).toBe(false); }); - it('should initialize the LaunchDarkly SDK instance', () => { - const initializeSpy = vi.spyOn(LDClient, 'initialize'); - ld.initialize('clientId', { key: 'user1' }); - expect(initializeSpy).toHaveBeenCalledWith('clientId', { key: 'user1' }); - }); - - it('should call waitUntilReady when initializing', () => { - const waitUntilReadySpy = vi.spyOn(mockLDClient, 'waitUntilReady'); - - ld.initialize('clientId', { key: 'user1' }); + it('should initialize the LaunchDarkly SDK instance', () => { + ld.initialize('clientId'); - expect(waitUntilReadySpy).toHaveBeenCalled(); + expect(initialize).toHaveBeenCalledWith('clientId'); }); - it('should register an event listener for the "change" event', () => { - const onSpy = vi.spyOn(mockLDClient, 'on'); + it('should register function that gets flag values when client is ready', () => { + const newFlags = { ...rawFlags, 'new-flag': true }; + const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(newFlags); - ld.initialize('clientId ', { key: 'user1' }); + ld.initialize('clientId'); + mockLDEventEmitter.emit('ready'); - expect(onSpy).toHaveBeenCalled(); - expect(onSpy).toHaveBeenCalledWith('change', expect.any(Function)); + expect(allFlagsSpy).toHaveBeenCalledOnce(); + expect(allFlagsSpy).toHaveReturnedWith(newFlags); }); - it('should set flags when the client is ready', () => { - const flagSubscriber = vi.fn(); - ld.initialize('clientId', { key: 'user1' }); + it('should register function that gets flag values when flags changed', () => { + const changedFlags = { ...rawFlags, 'changed-flag': true }; + const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(changedFlags); - const subscribeSpy = vi.spyOn(ld.flags, 'subscribe'); - ld.flags.subscribe(flagSubscriber); + ld.initialize('clientId'); + mockLDEventEmitter.emit('change'); - expect(subscribeSpy).toBeDefined(); - expect(flagSubscriber).toHaveBeenCalledTimes(1); - expect(flagSubscriber).toHaveBeenCalledWith(rawFlags); + expect(allFlagsSpy).toHaveBeenCalledOnce(); + expect(allFlagsSpy).toHaveReturnedWith(changedFlags); }); }); + describe('watch function', () => { const ld = LD; + beforeEach(() => { - mockInitialize.mockImplementation(() => mockLDClient); - mockAllFlags.mockImplementation(() => rawFlags); + // mocks the initialize function to return the mockLDClient + (initialize as Mock).mockReturnValue( + mockLDClient as unknown as LDClient, + ); + }); + + afterEach(() => { + vi.clearAllMocks(); + mockLDEventEmitter.removeAllListeners(); }); it('should return a derived store that reflects the value of the specified flag', () => { const flagKey = 'test-flag'; - ld.initialize(clientSideID, { key: 'user1' }); + ld.initialize(clientSideID); const flagStore = ld.watch(flagKey); @@ -130,25 +124,33 @@ describe('launchDarkly', () => { }); it('should update the flag store when the flag value changes', () => { - const flagKey = 'test-flag'; - ld.initialize(clientSideID, { key: 'user1' }); - - const flagStore = ld.watch(flagKey); + const booleanFlagKey = 'test-flag'; + const stringFlagKey = 'another-test-flag'; + ld.initialize(clientSideID); + const flagStore = ld.watch(booleanFlagKey); + const flagStore2 = ld.watch(stringFlagKey); + // 'test-flag' initial value is true according to `rawFlags` expect(get(flagStore)).toBe(true); + // 'another-test-flag' intial value is 'flag-value' according to `rawFlags` + expect(get(flagStore2)).toBe('flag-value'); - mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); + mockLDClient.allFlags.mockReturnValue({ + ...rawFlags, + 'test-flag': false, + 'another-test-flag': 'new-flag-value', + }); // dispatch a change event on ldClient - const changeCallback = mockLDClient.on.mock.calls[0][1]; - changeCallback(); + mockLDEventEmitter.emit('change'); expect(get(flagStore)).toBe(false); + expect(get(flagStore2)).toBe('new-flag-value'); }); it('should return undefined if the flag is not found', () => { const flagKey = 'non-existent-flag'; - ld.initialize(clientSideID, { key: 'user1' }); + ld.initialize(clientSideID); const flagStore = ld.watch(flagKey); @@ -156,89 +158,90 @@ describe('launchDarkly', () => { }); }); - describe('isOn function', () => { - const ld = LD; - beforeEach(() => { - mockInitialize.mockImplementation(() => mockLDClient); - mockAllFlags.mockImplementation(() => rawFlags); - }); + // TODO: fix these tests + // describe('isOn function', () => { + // const ld = LD; + // // beforeEach(() => { + // // mockInitialize.mockImplementation(() => mockLDClient); + // // mockAllFlags.mockImplementation(() => rawFlags); + // // }); - it('should return true if the flag is on', () => { - const flagKey = 'test-flag'; - ld.initialize(clientSideID, { key: 'user1' }); + // it('should return true if the flag is on', () => { + // const flagKey = 'test-flag'; + // ld.initialize(clientSideID, { key: 'user1' }); - expect(ld.isOn(flagKey)).toBe(true); - }); + // expect(ld.isOn(flagKey)).toBe(true); + // }); - it('should return false if the flag is off', () => { - const flagKey = 'test-flag'; - ld.initialize(clientSideID, { key: 'user1' }); + // it('should return false if the flag is off', () => { + // const flagKey = 'test-flag'; + // ld.initialize(clientSideID, { key: 'user1' }); - mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); + // mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); - // dispatch a change event on ldClient - const changeCallback = mockLDClient.on.mock.calls[0][1]; - changeCallback(); + // // dispatch a change event on ldClient + // const changeCallback = mockLDClient.on.mock.calls[0][1]; + // changeCallback(); - expect(ld.isOn(flagKey)).toBe(false); - }); + // expect(ld.isOn(flagKey)).toBe(false); + // }); - it('should return false if the flag is not found', () => { - const flagKey = 'non-existent-flag'; - ld.initialize(clientSideID, { key: 'user1' }); + // it('should return false if the flag is not found', () => { + // const flagKey = 'non-existent-flag'; + // ld.initialize(clientSideID, { key: 'user1' }); - expect(ld.isOn(flagKey)).toBe(false); - }); - }); + // expect(ld.isOn(flagKey)).toBe(false); + // }); + // }); - describe('identify function', () => { - const ld = LD; - beforeEach(() => { - mockInitialize.mockImplementation(() => mockLDClient); - mockAllFlags.mockImplementation(() => rawFlags); - }); + // describe('identify function', () => { + // const ld = LD; + // beforeEach(() => { + // mockInitialize.mockImplementation(() => mockLDClient); + // mockAllFlags.mockImplementation(() => rawFlags); + // }); - it('should call the identify method on the LaunchDarkly client', () => { - const user = { key: 'user1' }; - ld.initialize(clientSideID, user); + // it('should call the identify method on the LaunchDarkly client', () => { + // const user = { key: 'user1' }; + // ld.initialize(clientSideID, user); - ld.identify(user); + // ld.identify(user); - expect(mockLDClient.identify).toHaveBeenCalledWith(user); - }); - }); + // expect(mockLDClient.identify).toHaveBeenCalledWith(user); + // }); + // }); - describe('flags store', () => { - const ld = LD; - beforeEach(() => { - mockInitialize.mockImplementation(() => mockLDClient); - mockAllFlags.mockImplementation(() => rawFlags); - }); + // describe('flags store', () => { + // const ld = LD; + // beforeEach(() => { + // mockInitialize.mockImplementation(() => mockLDClient); + // mockAllFlags.mockImplementation(() => rawFlags); + // }); - it('should return a readonly store of the flags', () => { - ld.initialize(clientSideID, { key: 'user1' }); + // it('should return a readonly store of the flags', () => { + // ld.initialize(clientSideID, { key: 'user1' }); - const { flags } = ld; + // const { flags } = ld; - expect(get(flags)).toEqual(rawFlags); - }); + // expect(get(flags)).toEqual(rawFlags); + // }); - it('should update the flags store when the flags change', () => { - ld.initialize(clientSideID, { key: 'user1' }); + // it('should update the flags store when the flags change', () => { + // ld.initialize(clientSideID, { key: 'user1' }); - const { flags } = ld; + // const { flags } = ld; - expect(get(flags)).toEqual(rawFlags); + // expect(get(flags)).toEqual(rawFlags); - const newFlags = { 'test-flag': false, 'another-test-flag': true }; - mockAllFlags.mockReturnValue(newFlags); + // const newFlags = { 'test-flag': false, 'another-test-flag': true }; + // mockAllFlags.mockReturnValue(newFlags); - // dispatch a change event on ldClient - const changeCallback = mockLDClient.on.mock.calls[0][1]; - changeCallback(); + // // dispatch a change event on ldClient + // const changeCallback = mockLDClient.on.mock.calls[0][1]; + // changeCallback(); - expect(get(flags)).toEqual(newFlags); - }); - }); + // expect(get(flags)).toEqual(newFlags); + // }); + // }); }); }); diff --git a/packages/sdk/svelte/package.json b/packages/sdk/svelte/package.json index 86bd18b289..cf3914e34f 100644 --- a/packages/sdk/svelte/package.json +++ b/packages/sdk/svelte/package.json @@ -43,13 +43,14 @@ "test:unit-ui": "vitest --ui" }, "peerDependencies": { - "@launchdarkly/js-client-sdk-common": "^1.1.4", + "@launchdarkly/js-client-sdk": "workspace:^", + "@launchdarkly/js-client-sdk-common": "^1.10.0", "@launchdarkly/node-server-sdk": "^9.4.6", - "launchdarkly-js-client-sdk": "^3.4.0", "svelte": "^4.0.0" }, "dependencies": { - "@launchdarkly/js-client-sdk-common": "1.1.4", + "@launchdarkly/js-client-sdk": "workspace:^", + "@launchdarkly/js-client-sdk-common": "1.10.0", "esm-env": "^1.0.0" }, "devDependencies": { @@ -84,6 +85,6 @@ "typedoc": "0.25.0", "typescript": "5.1.6", "vite": "^5.2.6", - "vitest": "^1.6.0" + "vitest": "^2.1.4" } } diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index 3ec4515bde..a251a37d31 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -1,24 +1,21 @@ -import { initialize } from 'launchdarkly-js-client-sdk'; -import type { - LDClient, - LDFlagSet, - LDFlagValue, - LDContext as NodeLDContext, -} from 'launchdarkly-js-client-sdk'; import { derived, get, type Readable, readonly, writable, type Writable } from 'svelte/store'; +import { + initialize, + type LDClient, + type LDContext, + type LDFlagSet, +} from '@launchdarkly/js-client-sdk'; + /** Client ID for LaunchDarkly */ export type LDClientID = string; -/** Context for LaunchDarkly */ -export type LDContext = NodeLDContext; - -/** Value of LaunchDarkly flags */ -export type LDFlagsValue = LDFlagValue; - /** Flags for LaunchDarkly */ export type LDFlags = LDFlagSet; +/** Value of LaunchDarkly flags */ +export type LDFlagsValue = LDFlagSet[string]; + /** * Checks if the LaunchDarkly client is initialized. * @param {LDClient | undefined} client - The LaunchDarkly client. @@ -42,19 +39,20 @@ function createLD() { /** * Initializes the LaunchDarkly client. * @param {LDClientID} clientId - The client ID. - * @param {LDContext} context - The context. * @returns {Writable} An object with the initialization status store. */ - function LDInitialize(clientId: LDClientID, context: LDContext) { - jsSdk = initialize(clientId, context); - jsSdk.waitUntilReady().then(() => { + function LDInitialize(clientId: LDClientID) { + jsSdk = initialize(clientId); + jsSdk!.on('ready', () => { loading.set(false); - flagsWritable.set(jsSdk!.allFlags()); + const allFlags = jsSdk!.allFlags(); + flagsWritable.set(allFlags); }); - jsSdk.on('change', () => { - flagsWritable.set(jsSdk!.allFlags()); + jsSdk!.on('change', () => { + const allFlags = jsSdk!.allFlags(); + flagsWritable.set(allFlags); }); return { From 938314c847fe3c2458aabb70ef0fa9f2fd9b1998 Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Wed, 6 Nov 2024 18:53:10 -0300 Subject: [PATCH 04/12] refactor: update SvelteLDClient tests to use clientSideID --- .../lib/client/SvelteLDClient.test.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index b0f9279547..551d82852d 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -1,6 +1,6 @@ import { EventEmitter } from 'node:events'; import { get } from 'svelte/store'; -import { afterAll, afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; import { initialize, LDClient } from '@launchdarkly/js-client-sdk'; @@ -62,7 +62,7 @@ describe('launchDarkly', () => { it('should set the loading status to false when the client is ready', async () => { const { initializing } = ld; - ld.initialize('clientId'); + ld.initialize(clientSideID); expect(get(initializing)).toBe(true); // should be true before the ready event is emitted mockLDEventEmitter.emit('ready'); @@ -71,16 +71,16 @@ describe('launchDarkly', () => { }); it('should initialize the LaunchDarkly SDK instance', () => { - ld.initialize('clientId'); + ld.initialize(clientSideID); - expect(initialize).toHaveBeenCalledWith('clientId'); + expect(initialize).toHaveBeenCalledWith('test-client-side-id'); }); it('should register function that gets flag values when client is ready', () => { const newFlags = { ...rawFlags, 'new-flag': true }; const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(newFlags); - ld.initialize('clientId'); + ld.initialize(clientSideID); mockLDEventEmitter.emit('ready'); expect(allFlagsSpy).toHaveBeenCalledOnce(); @@ -91,7 +91,7 @@ describe('launchDarkly', () => { const changedFlags = { ...rawFlags, 'changed-flag': true }; const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(changedFlags); - ld.initialize('clientId'); + ld.initialize(clientSideID); mockLDEventEmitter.emit('change'); expect(allFlagsSpy).toHaveBeenCalledOnce(); @@ -161,21 +161,29 @@ describe('launchDarkly', () => { // TODO: fix these tests // describe('isOn function', () => { // const ld = LD; - // // beforeEach(() => { - // // mockInitialize.mockImplementation(() => mockLDClient); - // // mockAllFlags.mockImplementation(() => rawFlags); - // // }); + + // beforeEach(() => { + // // mocks the initialize function to return the mockLDClient + // (initialize as Mock).mockReturnValue( + // mockLDClient as unknown as LDClient, + // ); + // }); + + // afterEach(() => { + // vi.clearAllMocks(); + // mockLDEventEmitter.removeAllListeners(); + // }); // it('should return true if the flag is on', () => { // const flagKey = 'test-flag'; - // ld.initialize(clientSideID, { key: 'user1' }); + // ld.initialize(clientSideID); // expect(ld.isOn(flagKey)).toBe(true); // }); // it('should return false if the flag is off', () => { // const flagKey = 'test-flag'; - // ld.initialize(clientSideID, { key: 'user1' }); + // ld.initialize(clientSideID); // mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); From 36903397a465b09964b0119004e9adc9dbf7868d Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Fri, 29 Nov 2024 14:41:36 -0300 Subject: [PATCH 05/12] refactor: update SvelteLDClient to use proxy for flag variations and improve initialization logic --- .../lib/client/SvelteLDClient.test.ts | 5 +- packages/sdk/svelte/package.json | 2 - .../svelte/src/lib/client/SvelteLDClient.ts | 51 +++++++++++++++---- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index 551d82852d..cf8fdd3af5 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -18,7 +18,7 @@ const mockLDClient = { on: (e: string, cb: () => void) => mockLDEventEmitter.on(e, cb), off: vi.fn(), allFlags: vi.fn().mockReturnValue(rawFlags), - variation: vi.fn(), + variation: vi.fn((_, defaultValue) => defaultValue), identify: vi.fn(), }; @@ -130,6 +130,9 @@ describe('launchDarkly', () => { const flagStore = ld.watch(booleanFlagKey); const flagStore2 = ld.watch(stringFlagKey); + // emit ready event to set initial flag values + mockLDEventEmitter.emit('ready'); + // 'test-flag' initial value is true according to `rawFlags` expect(get(flagStore)).toBe(true); // 'another-test-flag' intial value is 'flag-value' according to `rawFlags` diff --git a/packages/sdk/svelte/package.json b/packages/sdk/svelte/package.json index cf3914e34f..bb972b19c1 100644 --- a/packages/sdk/svelte/package.json +++ b/packages/sdk/svelte/package.json @@ -44,13 +44,11 @@ }, "peerDependencies": { "@launchdarkly/js-client-sdk": "workspace:^", - "@launchdarkly/js-client-sdk-common": "^1.10.0", "@launchdarkly/node-server-sdk": "^9.4.6", "svelte": "^4.0.0" }, "dependencies": { "@launchdarkly/js-client-sdk": "workspace:^", - "@launchdarkly/js-client-sdk-common": "1.10.0", "esm-env": "^1.0.0" }, "devDependencies": { diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index a251a37d31..0b31030569 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -27,12 +27,43 @@ function isClientInitialized(client: LDClient | undefined): asserts client is LD } } +/** + * Creates a proxy for the given flags object that intercepts access to flag values. + * When a flag value is accessed, it checks if the flag key exists in the target object. + * If the flag key exists, it returns the variation of the flag from the client. + * Otherwise, it returns the current value of the flag. + * + * @param client - The LaunchDarkly client instance used to get flag variations. + * @param flags - The initial flags object to be proxied. + * @returns A proxy object that intercepts access to flag values and returns the appropriate variation. + */ +function toFlagsProxy(client: LDClient, flags: LDFlags): LDFlags { + return new Proxy(flags, { + get(target, prop, receiver) { + const currentValue = Reflect.get(target, prop, receiver); + // only process flag keys and ignore symbols and native Object functions + if (typeof prop === 'symbol') { + return currentValue; + } + + // check if flag key exists + const validFlagKey = Object.hasOwn(target, prop); + + if (!validFlagKey) { + return currentValue; + } + + return client.variation(prop, currentValue); + }, + }); +} + /** * Creates a LaunchDarkly instance. * @returns {Object} The LaunchDarkly instance object. */ function createLD() { - let jsSdk: LDClient | undefined; + let coreLdClient: LDClient | undefined; const loading = writable(true); const flagsWritable = writable({}); @@ -43,15 +74,17 @@ function createLD() { */ function LDInitialize(clientId: LDClientID) { - jsSdk = initialize(clientId); - jsSdk!.on('ready', () => { + coreLdClient = initialize(clientId); + coreLdClient!.on('ready', () => { loading.set(false); - const allFlags = jsSdk!.allFlags(); + const rawFlags = coreLdClient!.allFlags(); + const allFlags = toFlagsProxy(coreLdClient, rawFlags); flagsWritable.set(allFlags); }); - jsSdk!.on('change', () => { - const allFlags = jsSdk!.allFlags(); + coreLdClient!.on('change', () => { + const rawFlags = coreLdClient!.allFlags(); + const allFlags = toFlagsProxy(coreLdClient, rawFlags); flagsWritable.set(allFlags); }); @@ -66,8 +99,8 @@ function createLD() { * @returns {Promise} A promise that resolves when the user is identified. */ async function identify(context: LDContext) { - isClientInitialized(jsSdk); - return jsSdk.identify(context); + isClientInitialized(coreLdClient); + return coreLdClient.identify(context); } /** @@ -84,7 +117,7 @@ function createLD() { * @returns {boolean} True if the flag is on, false otherwise. */ const isOn = (flagKey: string): boolean => { - isClientInitialized(jsSdk); + isClientInitialized(coreLdClient); const currentFlags = get(flagsWritable); return !!currentFlags[flagKey]; }; From cf495712d12377b79fe86478f3d4214a68da800a Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Tue, 3 Dec 2024 18:27:45 -0300 Subject: [PATCH 06/12] refactor: update SvelteLDClient to use compat SDK and improve initialization with user context --- .../lib/client/SvelteLDClient.test.ts | 21 ++++++++------- packages/sdk/svelte/package.json | 11 ++++---- .../svelte/src/lib/client/SvelteLDClient.ts | 26 +++++++++---------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index cf8fdd3af5..490bbbdd89 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -2,14 +2,15 @@ import { EventEmitter } from 'node:events'; import { get } from 'svelte/store'; import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest'; -import { initialize, LDClient } from '@launchdarkly/js-client-sdk'; +import { initialize, LDClient } from '@launchdarkly/js-client-sdk/compat'; import { LD } from '../../../src/lib/client/SvelteLDClient'; -vi.mock('@launchdarkly/js-client-sdk', { spy: true }); +vi.mock('@launchdarkly/js-client-sdk/compat', { spy: true }); const clientSideID = 'test-client-side-id'; const rawFlags = { 'test-flag': true, 'another-test-flag': 'flag-value' }; +const mockContext = { key: 'user1' }; // used to mock ready and change events on the LDClient const mockLDEventEmitter = new EventEmitter(); @@ -62,7 +63,7 @@ describe('launchDarkly', () => { it('should set the loading status to false when the client is ready', async () => { const { initializing } = ld; - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); expect(get(initializing)).toBe(true); // should be true before the ready event is emitted mockLDEventEmitter.emit('ready'); @@ -71,16 +72,16 @@ describe('launchDarkly', () => { }); it('should initialize the LaunchDarkly SDK instance', () => { - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); - expect(initialize).toHaveBeenCalledWith('test-client-side-id'); + expect(initialize).toHaveBeenCalledWith('test-client-side-id', mockContext); }); it('should register function that gets flag values when client is ready', () => { const newFlags = { ...rawFlags, 'new-flag': true }; const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(newFlags); - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); mockLDEventEmitter.emit('ready'); expect(allFlagsSpy).toHaveBeenCalledOnce(); @@ -91,7 +92,7 @@ describe('launchDarkly', () => { const changedFlags = { ...rawFlags, 'changed-flag': true }; const allFlagsSpy = vi.spyOn(mockLDClient, 'allFlags').mockReturnValue(changedFlags); - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); mockLDEventEmitter.emit('change'); expect(allFlagsSpy).toHaveBeenCalledOnce(); @@ -116,7 +117,7 @@ describe('launchDarkly', () => { it('should return a derived store that reflects the value of the specified flag', () => { const flagKey = 'test-flag'; - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); const flagStore = ld.watch(flagKey); @@ -126,7 +127,7 @@ describe('launchDarkly', () => { it('should update the flag store when the flag value changes', () => { const booleanFlagKey = 'test-flag'; const stringFlagKey = 'another-test-flag'; - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); const flagStore = ld.watch(booleanFlagKey); const flagStore2 = ld.watch(stringFlagKey); @@ -153,7 +154,7 @@ describe('launchDarkly', () => { it('should return undefined if the flag is not found', () => { const flagKey = 'non-existent-flag'; - ld.initialize(clientSideID); + ld.initialize(clientSideID, mockContext); const flagStore = ld.watch(flagKey); diff --git a/packages/sdk/svelte/package.json b/packages/sdk/svelte/package.json index bb972b19c1..8061f94be9 100644 --- a/packages/sdk/svelte/package.json +++ b/packages/sdk/svelte/package.json @@ -44,7 +44,6 @@ }, "peerDependencies": { "@launchdarkly/js-client-sdk": "workspace:^", - "@launchdarkly/node-server-sdk": "^9.4.6", "svelte": "^4.0.0" }, "dependencies": { @@ -56,12 +55,12 @@ "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/kit": "^2.0.0", "@sveltejs/package": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^5.0.1", "@testing-library/svelte": "^5.2.0", "@types/jest": "^29.5.11", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", - "@vitest/ui": "^1.6.0", + "@vitest/ui": "^2.1.8", "eslint": "^8.45.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", @@ -76,13 +75,13 @@ "prettier-plugin-svelte": "^3.1.2", "publint": "^0.1.9", "rimraf": "^5.0.5", - "svelte": "^4.2.7", + "svelte": "^5.4.0", "svelte-check": "^3.6.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typedoc": "0.25.0", "typescript": "5.1.6", - "vite": "^5.2.6", - "vitest": "^2.1.4" + "vite": "^6.0.2", + "vitest": "^2.1.8" } } diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index 0b31030569..2dbdc84ca3 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -1,11 +1,14 @@ import { derived, get, type Readable, readonly, writable, type Writable } from 'svelte/store'; +import type { LDFlagSet } from '@launchdarkly/js-client-sdk'; import { initialize, type LDClient, type LDContext, - type LDFlagSet, -} from '@launchdarkly/js-client-sdk'; + type LDFlagValue, +} from '@launchdarkly/js-client-sdk/compat'; + +export type { LDContext }; /** Client ID for LaunchDarkly */ export type LDClientID = string; @@ -13,9 +16,6 @@ export type LDClientID = string; /** Flags for LaunchDarkly */ export type LDFlags = LDFlagSet; -/** Value of LaunchDarkly flags */ -export type LDFlagsValue = LDFlagSet[string]; - /** * Checks if the LaunchDarkly client is initialized. * @param {LDClient | undefined} client - The LaunchDarkly client. @@ -70,21 +70,21 @@ function createLD() { /** * Initializes the LaunchDarkly client. * @param {LDClientID} clientId - The client ID. - * @returns {Writable} An object with the initialization status store. + * @param {LDContext} context - The user context. + * @returns {Object} An object with the initialization status store. */ - - function LDInitialize(clientId: LDClientID) { - coreLdClient = initialize(clientId); + function LDInitialize(clientId: LDClientID, context: LDContext) { + coreLdClient = initialize(clientId, context); coreLdClient!.on('ready', () => { loading.set(false); const rawFlags = coreLdClient!.allFlags(); - const allFlags = toFlagsProxy(coreLdClient, rawFlags); + const allFlags = toFlagsProxy(coreLdClient!, rawFlags); flagsWritable.set(allFlags); }); coreLdClient!.on('change', () => { const rawFlags = coreLdClient!.allFlags(); - const allFlags = toFlagsProxy(coreLdClient, rawFlags); + const allFlags = toFlagsProxy(coreLdClient!, rawFlags); flagsWritable.set(allFlags); }); @@ -108,8 +108,8 @@ function createLD() { * @param {string} flagKey - The key of the flag to watch. * @returns {Readable} A readable store of the flag value. */ - const watch = (flagKey: string): Readable => - derived, LDFlagsValue>(flagsWritable, ($flags) => $flags[flagKey]); + const watch = (flagKey: string): Readable => + derived, LDFlagValue>(flagsWritable, ($flags) => $flags[flagKey]); /** * Checks if a flag is on. From 1572639b7581b3b7d9b2693cd61000eb3025ae73 Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Wed, 4 Dec 2024 19:25:42 -0300 Subject: [PATCH 07/12] feat: add Svelte example project with configuration and initial tests --- package.json | 1 + packages/sdk/svelte/example/.eslintignore | 13 ++ packages/sdk/svelte/example/.eslintrc.cjs | 31 +++++ packages/sdk/svelte/example/.gitignore | 11 ++ packages/sdk/svelte/example/.npmrc | 1 + packages/sdk/svelte/example/.prettierignore | 4 + packages/sdk/svelte/example/.prettierrc | 8 ++ packages/sdk/svelte/example/README.md | 131 ++++++++++++++++++ packages/sdk/svelte/example/package.json | 49 +++++++ packages/sdk/svelte/example/src/app.d.ts | 13 ++ packages/sdk/svelte/example/src/app.html | 12 ++ packages/sdk/svelte/example/src/index.test.ts | 7 + .../svelte/example/src/routes/+layout.svelte | 18 +++ .../svelte/example/src/routes/+page.svelte | 26 ++++ .../sdk/svelte/example/static/favicon.png | Bin 0 -> 1571 bytes packages/sdk/svelte/example/svelte.config.js | 18 +++ packages/sdk/svelte/example/tsconfig.json | 15 ++ packages/sdk/svelte/example/vite.config.ts | 11 ++ packages/sdk/svelte/package.json | 2 - 19 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/svelte/example/.eslintignore create mode 100644 packages/sdk/svelte/example/.eslintrc.cjs create mode 100644 packages/sdk/svelte/example/.gitignore create mode 100644 packages/sdk/svelte/example/.npmrc create mode 100644 packages/sdk/svelte/example/.prettierignore create mode 100644 packages/sdk/svelte/example/.prettierrc create mode 100644 packages/sdk/svelte/example/README.md create mode 100644 packages/sdk/svelte/example/package.json create mode 100644 packages/sdk/svelte/example/src/app.d.ts create mode 100644 packages/sdk/svelte/example/src/app.html create mode 100644 packages/sdk/svelte/example/src/index.test.ts create mode 100644 packages/sdk/svelte/example/src/routes/+layout.svelte create mode 100644 packages/sdk/svelte/example/src/routes/+page.svelte create mode 100644 packages/sdk/svelte/example/static/favicon.png create mode 100644 packages/sdk/svelte/example/svelte.config.js create mode 100644 packages/sdk/svelte/example/tsconfig.json create mode 100644 packages/sdk/svelte/example/vite.config.ts diff --git a/package.json b/package.json index b7b0bdc24b..bf8d609aef 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "packages/sdk/react-universal/example", "packages/sdk/vercel", "packages/sdk/svelte", + "packages/sdk/svelte/example", "packages/sdk/akamai-base", "packages/sdk/akamai-base/example", "packages/sdk/akamai-edgekv", diff --git a/packages/sdk/svelte/example/.eslintignore b/packages/sdk/svelte/example/.eslintignore new file mode 100644 index 0000000000..38972655fa --- /dev/null +++ b/packages/sdk/svelte/example/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/packages/sdk/svelte/example/.eslintrc.cjs b/packages/sdk/svelte/example/.eslintrc.cjs new file mode 100644 index 0000000000..0b757582c0 --- /dev/null +++ b/packages/sdk/svelte/example/.eslintrc.cjs @@ -0,0 +1,31 @@ +/** @type { import("eslint").Linter.Config } */ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'] + }, + env: { + browser: true, + es2017: true, + node: true + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser' + } + } + ] +}; diff --git a/packages/sdk/svelte/example/.gitignore b/packages/sdk/svelte/example/.gitignore new file mode 100644 index 0000000000..ac7211b403 --- /dev/null +++ b/packages/sdk/svelte/example/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +node_modules +/build +/dist +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/packages/sdk/svelte/example/.npmrc b/packages/sdk/svelte/example/.npmrc new file mode 100644 index 0000000000..b6f27f1359 --- /dev/null +++ b/packages/sdk/svelte/example/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/packages/sdk/svelte/example/.prettierignore b/packages/sdk/svelte/example/.prettierignore new file mode 100644 index 0000000000..cc41cea9b2 --- /dev/null +++ b/packages/sdk/svelte/example/.prettierignore @@ -0,0 +1,4 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/packages/sdk/svelte/example/.prettierrc b/packages/sdk/svelte/example/.prettierrc new file mode 100644 index 0000000000..95730232b6 --- /dev/null +++ b/packages/sdk/svelte/example/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/packages/sdk/svelte/example/README.md b/packages/sdk/svelte/example/README.md new file mode 100644 index 0000000000..55671a5ad8 --- /dev/null +++ b/packages/sdk/svelte/example/README.md @@ -0,0 +1,131 @@ +# Launch Darkly Svelte SDK + +This is a Svelte library for Launch Darkly. It is a wrapper around the official Launch Darkly JavaScript SDK but with a Svelte-friendly API. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Advanced Usage](#advanced-usage) + - [Changing user context](#changing-user-context) + - [Getting feature flag values](#getting-feature-flag-values) + - [Getting immediate flag value](#getting-immediate-flag-value) + - [Watching flag value changes](#watching-flag-value-changes) + - [Getting all flag values](#getting-all-flag-values) + +## Getting started + +First, install the package: + +```bash +npm install launchdarkly-svelte-client-sdk # or use yarn or pnpm +``` + +Then, initialize the SDK with your client-side ID using the `LDProvider` component: + +```svelte + + +// Use context relevant to your application +const context = { + user: { + key: 'user-key', + }, +}; + + + + +``` + +Now you can use the `LDFlag` component to conditionally render content based on feature flags: + +```svelte + + + +
+

this will render if the feature flag is on

+
+
+

this will render if the feature flag is off

+
+
+``` + +## Advanced usage + +### Changing user context + +You can change the user context by using the `identify` function from the `LD` object: + +```svelte + + + +``` + +### Getting feature flag values + +#### Getting immediate flag value + +If you need to get the value of a flag at time of evaluation you can use the `isOn` function: + +```svelte + + + +``` + +**Note:** Please note that `isOn` function will return the current value of the flag at the time of evaluation, which means you won't get notified if the flag value changes. This is useful for cases where you need to get the value of a flag at a specific time like a function call. If you need to get notified when the flag value changes, you should use the `LDFlag` component, the `watch` function or the `flags` object depending on you use case. + +#### Watching flag value changes + +If you need to get notified when a flag value changes you can use the `watch` function. The `watch` function is an instance of [Svelte Store](https://svelte.dev/docs/svelte-store), which means you can use it with the `$` store subscriber syntax or the `subscribe` method. Here is an example of how to use the `watch` function: + +```svelte + + +

{$flagValue}

+``` + +#### Getting all flag values + +If you need to get all flag values you can use the `flags` object. The `flags` object is an instance of [Svelte Store](https://svelte.dev/docs/svelte-store), which means you can use it with the `$` store subscriber syntax or the `subscribe` method. Here is an example of how to use the `flags` object: + +```svelte + + +{#each Object.keys($allFlags) as flagName} +

{flagName}: {$allFlags[flagName]}

+{/each} +``` + +## Credits + +- Original code by [Robinson Marquez](https://github.com/nosnibor89) diff --git a/packages/sdk/svelte/example/package.json b/packages/sdk/svelte/example/package.json new file mode 100644 index 0000000000..7d0b084570 --- /dev/null +++ b/packages/sdk/svelte/example/package.json @@ -0,0 +1,49 @@ +{ + "name": "ld-svelte-example", + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/**/*.test.*", + "!dist/**/*.spec.*" + ], + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "type": "module", + "dependencies": { + "@launchdarkly/svelte-client-sdk": "workspace:^", + "esm-env": "^1.0.0", + "svelte": "^5.4.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/package": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^5.0.1", + "@types/eslint": "8.56.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.35.1", + "jsdom": "^24.0.0", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "svelte-check": "^3.6.0", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^6.0.2", + "vitest": "^2.1.8" + } +} diff --git a/packages/sdk/svelte/example/src/app.d.ts b/packages/sdk/svelte/example/src/app.d.ts new file mode 100644 index 0000000000..743f07b2e5 --- /dev/null +++ b/packages/sdk/svelte/example/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/packages/sdk/svelte/example/src/app.html b/packages/sdk/svelte/example/src/app.html new file mode 100644 index 0000000000..f22aeaad5e --- /dev/null +++ b/packages/sdk/svelte/example/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/packages/sdk/svelte/example/src/index.test.ts b/packages/sdk/svelte/example/src/index.test.ts new file mode 100644 index 0000000000..e07cbbd725 --- /dev/null +++ b/packages/sdk/svelte/example/src/index.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/packages/sdk/svelte/example/src/routes/+layout.svelte b/packages/sdk/svelte/example/src/routes/+layout.svelte new file mode 100644 index 0000000000..2229218176 --- /dev/null +++ b/packages/sdk/svelte/example/src/routes/+layout.svelte @@ -0,0 +1,18 @@ + + +
+ + + +

loading flags...

+
+
diff --git a/packages/sdk/svelte/example/src/routes/+page.svelte b/packages/sdk/svelte/example/src/routes/+page.svelte new file mode 100644 index 0000000000..9595bd334f --- /dev/null +++ b/packages/sdk/svelte/example/src/routes/+page.svelte @@ -0,0 +1,26 @@ + + + + + +
+

this box is lightblue when flag is on ({PUBLIC_LD_FLAG_KEY})

+
+
+

this box is lightyellow when flag is off ({PUBLIC_LD_FLAG_KEY})

+
+
+ + diff --git a/packages/sdk/svelte/example/static/favicon.png b/packages/sdk/svelte/example/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Date: Thu, 5 Dec 2024 20:10:45 -0300 Subject: [PATCH 08/12] docs: add README files for LaunchDarkly Svelte SDK and example project --- packages/sdk/svelte/README.md | 131 ++++++++++++++++++++++++++ packages/sdk/svelte/example/README.md | 131 ++++---------------------- 2 files changed, 149 insertions(+), 113 deletions(-) create mode 100644 packages/sdk/svelte/README.md diff --git a/packages/sdk/svelte/README.md b/packages/sdk/svelte/README.md new file mode 100644 index 0000000000..55671a5ad8 --- /dev/null +++ b/packages/sdk/svelte/README.md @@ -0,0 +1,131 @@ +# Launch Darkly Svelte SDK + +This is a Svelte library for Launch Darkly. It is a wrapper around the official Launch Darkly JavaScript SDK but with a Svelte-friendly API. + +## Table of Contents + +- [Getting Started](#getting-started) +- [Advanced Usage](#advanced-usage) + - [Changing user context](#changing-user-context) + - [Getting feature flag values](#getting-feature-flag-values) + - [Getting immediate flag value](#getting-immediate-flag-value) + - [Watching flag value changes](#watching-flag-value-changes) + - [Getting all flag values](#getting-all-flag-values) + +## Getting started + +First, install the package: + +```bash +npm install launchdarkly-svelte-client-sdk # or use yarn or pnpm +``` + +Then, initialize the SDK with your client-side ID using the `LDProvider` component: + +```svelte + + +// Use context relevant to your application +const context = { + user: { + key: 'user-key', + }, +}; + + + + +``` + +Now you can use the `LDFlag` component to conditionally render content based on feature flags: + +```svelte + + + +
+

this will render if the feature flag is on

+
+
+

this will render if the feature flag is off

+
+
+``` + +## Advanced usage + +### Changing user context + +You can change the user context by using the `identify` function from the `LD` object: + +```svelte + + + +``` + +### Getting feature flag values + +#### Getting immediate flag value + +If you need to get the value of a flag at time of evaluation you can use the `isOn` function: + +```svelte + + + +``` + +**Note:** Please note that `isOn` function will return the current value of the flag at the time of evaluation, which means you won't get notified if the flag value changes. This is useful for cases where you need to get the value of a flag at a specific time like a function call. If you need to get notified when the flag value changes, you should use the `LDFlag` component, the `watch` function or the `flags` object depending on you use case. + +#### Watching flag value changes + +If you need to get notified when a flag value changes you can use the `watch` function. The `watch` function is an instance of [Svelte Store](https://svelte.dev/docs/svelte-store), which means you can use it with the `$` store subscriber syntax or the `subscribe` method. Here is an example of how to use the `watch` function: + +```svelte + + +

{$flagValue}

+``` + +#### Getting all flag values + +If you need to get all flag values you can use the `flags` object. The `flags` object is an instance of [Svelte Store](https://svelte.dev/docs/svelte-store), which means you can use it with the `$` store subscriber syntax or the `subscribe` method. Here is an example of how to use the `flags` object: + +```svelte + + +{#each Object.keys($allFlags) as flagName} +

{flagName}: {$allFlags[flagName]}

+{/each} +``` + +## Credits + +- Original code by [Robinson Marquez](https://github.com/nosnibor89) diff --git a/packages/sdk/svelte/example/README.md b/packages/sdk/svelte/example/README.md index 55671a5ad8..c942c17c90 100644 --- a/packages/sdk/svelte/example/README.md +++ b/packages/sdk/svelte/example/README.md @@ -1,131 +1,36 @@ -# Launch Darkly Svelte SDK +# LaunchDarkly Svelte SDK Example -This is a Svelte library for Launch Darkly. It is a wrapper around the official Launch Darkly JavaScript SDK but with a Svelte-friendly API. +This project demonstrates the usage of the `@launchdarkly/svelte-client-sdk`. It showcases how to conditionally render content based on feature flags using the `LDFlag` component. -## Table of Contents +## Installing Dependencies and Setting Environment Variables -- [Getting Started](#getting-started) -- [Advanced Usage](#advanced-usage) - - [Changing user context](#changing-user-context) - - [Getting feature flag values](#getting-feature-flag-values) - - [Getting immediate flag value](#getting-immediate-flag-value) - - [Watching flag value changes](#watching-flag-value-changes) - - [Getting all flag values](#getting-all-flag-values) - -## Getting started - -First, install the package: +First, install the project dependencies: ```bash -npm install launchdarkly-svelte-client-sdk # or use yarn or pnpm -``` - -Then, initialize the SDK with your client-side ID using the `LDProvider` component: - -```svelte - - -// Use context relevant to your application -const context = { - user: { - key: 'user-key', - }, -}; - - - - -``` - -Now you can use the `LDFlag` component to conditionally render content based on feature flags: - -```svelte - - - -
-

this will render if the feature flag is on

-
-
-

this will render if the feature flag is off

-
-
-``` - -## Advanced usage - -### Changing user context - -You can change the user context by using the `identify` function from the `LD` object: - -```svelte - - - +yarn install ``` -### Getting feature flag values - -#### Getting immediate flag value +Next, create a `.env` file in the root of the project and add your LaunchDarkly client-side ID and flag key. You can obtain these from any LaunchDarkly project/environment you choose. -If you need to get the value of a flag at time of evaluation you can use the `isOn` function: - -```svelte - - - +```bash +PUBLIC_LD_CLIENT_SIDE_ID=your-client-side-id +PUBLIC_LD_FLAG_KEY=your-flag-key ``` -**Note:** Please note that `isOn` function will return the current value of the flag at the time of evaluation, which means you won't get notified if the flag value changes. This is useful for cases where you need to get the value of a flag at a specific time like a function call. If you need to get notified when the flag value changes, you should use the `LDFlag` component, the `watch` function or the `flags` object depending on you use case. +Note: The flag specified by `PUBLIC_LD_FLAG_KEY` must be a boolean flag. -#### Watching flag value changes +## Running the Project -If you need to get notified when a flag value changes you can use the `watch` function. The `watch` function is an instance of [Svelte Store](https://svelte.dev/docs/svelte-store), which means you can use it with the `$` store subscriber syntax or the `subscribe` method. Here is an example of how to use the `watch` function: +To run the project, use the following command: -```svelte - - -

{$flagValue}

+```bash +yarn dev ``` -#### Getting all flag values - -If you need to get all flag values you can use the `flags` object. The `flags` object is an instance of [Svelte Store](https://svelte.dev/docs/svelte-store), which means you can use it with the `$` store subscriber syntax or the `subscribe` method. Here is an example of how to use the `flags` object: +This will start the development server. Open your browser and navigate to the provided URL to see the example in action. The box will change its background color based on the value of the feature flag specified by `PUBLIC_LD_FLAG_KEY`. -```svelte - - -{#each Object.keys($allFlags) as flagName} -

{flagName}: {$allFlags[flagName]}

-{/each} -``` +### Role of `LDProvider` -## Credits +The `LDProvider` component is used to initialize the LaunchDarkly client and provide the feature flag context to the rest of the application. It requires a `clientID` and a `context` object. The `context` object typically contains information about the user or environment, which LaunchDarkly uses to determine the state of feature flags. -- Original code by [Robinson Marquez](https://github.com/nosnibor89) +In this example, the `LDProvider` wraps the entire application, ensuring that all child components have access to the feature flag data. The `slot="initializing"` is used to display a loading message while the flags are being fetched. From ff0b51bc58acf4b80d4deb7f62759c22fbef575f Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Mon, 9 Dec 2024 21:01:44 -0300 Subject: [PATCH 09/12] refactor: replace isOn with useFlag in SvelteLDClient and update tests --- .../lib/client/SvelteLDClient.test.ts | 126 ++++++------------ packages/sdk/svelte/package.json | 4 +- packages/sdk/svelte/src/lib/LDFlag.svelte | 4 +- .../svelte/src/lib/client/SvelteLDClient.ts | 20 +-- 4 files changed, 56 insertions(+), 98 deletions(-) diff --git a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts index 490bbbdd89..ea689d6eda 100644 --- a/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts +++ b/packages/sdk/svelte/__tests__/lib/client/SvelteLDClient.test.ts @@ -33,7 +33,7 @@ describe('launchDarkly', () => { expect(ld).toHaveProperty('initialize'); expect(ld).toHaveProperty('initializing'); expect(ld).toHaveProperty('watch'); - expect(ld).toHaveProperty('isOn'); + expect(ld).toHaveProperty('useFlag'); }); describe('initialize', async () => { @@ -55,7 +55,7 @@ describe('launchDarkly', () => { const flagKey = 'test-flag'; const user = { key: 'user1' }; - expect(() => ld.isOn(flagKey)).toThrow('LaunchDarkly client not initialized'); + expect(() => ld.useFlag(flagKey, true)).toThrow('LaunchDarkly client not initialized'); await expect(() => ld.identify(user)).rejects.toThrow( 'LaunchDarkly client not initialized', ); @@ -162,98 +162,54 @@ describe('launchDarkly', () => { }); }); - // TODO: fix these tests - // describe('isOn function', () => { - // const ld = LD; - - // beforeEach(() => { - // // mocks the initialize function to return the mockLDClient - // (initialize as Mock).mockReturnValue( - // mockLDClient as unknown as LDClient, - // ); - // }); - - // afterEach(() => { - // vi.clearAllMocks(); - // mockLDEventEmitter.removeAllListeners(); - // }); - - // it('should return true if the flag is on', () => { - // const flagKey = 'test-flag'; - // ld.initialize(clientSideID); - - // expect(ld.isOn(flagKey)).toBe(true); - // }); - - // it('should return false if the flag is off', () => { - // const flagKey = 'test-flag'; - // ld.initialize(clientSideID); - - // mockAllFlags.mockReturnValue({ ...rawFlags, 'test-flag': false }); - - // // dispatch a change event on ldClient - // const changeCallback = mockLDClient.on.mock.calls[0][1]; - // changeCallback(); - - // expect(ld.isOn(flagKey)).toBe(false); - // }); - - // it('should return false if the flag is not found', () => { - // const flagKey = 'non-existent-flag'; - // ld.initialize(clientSideID, { key: 'user1' }); - - // expect(ld.isOn(flagKey)).toBe(false); - // }); - // }); - - // describe('identify function', () => { - // const ld = LD; - // beforeEach(() => { - // mockInitialize.mockImplementation(() => mockLDClient); - // mockAllFlags.mockImplementation(() => rawFlags); - // }); - - // it('should call the identify method on the LaunchDarkly client', () => { - // const user = { key: 'user1' }; - // ld.initialize(clientSideID, user); - - // ld.identify(user); - - // expect(mockLDClient.identify).toHaveBeenCalledWith(user); - // }); - // }); + describe('useFlag function', () => { + const ld = LD; - // describe('flags store', () => { - // const ld = LD; - // beforeEach(() => { - // mockInitialize.mockImplementation(() => mockLDClient); - // mockAllFlags.mockImplementation(() => rawFlags); - // }); + beforeEach(() => { + // mocks the initialize function to return the mockLDClient + (initialize as Mock).mockReturnValue( + mockLDClient as unknown as LDClient, + ); + }); - // it('should return a readonly store of the flags', () => { - // ld.initialize(clientSideID, { key: 'user1' }); + afterEach(() => { + vi.clearAllMocks(); + mockLDEventEmitter.removeAllListeners(); + }); - // const { flags } = ld; + it('should return flag value', () => { + mockLDClient.variation.mockReturnValue(true); + const flagKey = 'test-flag'; + ld.initialize(clientSideID, mockContext); - // expect(get(flags)).toEqual(rawFlags); - // }); + expect(ld.useFlag(flagKey, false)).toBe(true); + expect(mockLDClient.variation).toHaveBeenCalledWith(flagKey, false); + }); + }); - // it('should update the flags store when the flags change', () => { - // ld.initialize(clientSideID, { key: 'user1' }); + describe('identify function', () => { + const ld = LD; - // const { flags } = ld; + beforeEach(() => { + // mocks the initialize function to return the mockLDClient + (initialize as Mock).mockReturnValue( + mockLDClient as unknown as LDClient, + ); + }); - // expect(get(flags)).toEqual(rawFlags); + afterEach(() => { + vi.clearAllMocks(); + mockLDEventEmitter.removeAllListeners(); + }); - // const newFlags = { 'test-flag': false, 'another-test-flag': true }; - // mockAllFlags.mockReturnValue(newFlags); + it('should call the identify method on the LaunchDarkly client', () => { + const user = { key: 'user1' }; + ld.initialize(clientSideID, user); - // // dispatch a change event on ldClient - // const changeCallback = mockLDClient.on.mock.calls[0][1]; - // changeCallback(); + ld.identify(user); - // expect(get(flags)).toEqual(newFlags); - // }); - // }); + expect(mockLDClient.identify).toHaveBeenCalledWith(user); + }); + }); }); }); diff --git a/packages/sdk/svelte/package.json b/packages/sdk/svelte/package.json index 8061f94be9..10be950757 100644 --- a/packages/sdk/svelte/package.json +++ b/packages/sdk/svelte/package.json @@ -40,7 +40,8 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test", "test": "playwright test", "test:unit": "vitest", - "test:unit-ui": "vitest --ui" + "test:unit-ui": "vitest --ui", + "test:unit-coverage": "vitest --coverage" }, "peerDependencies": { "@launchdarkly/js-client-sdk": "workspace:^", @@ -60,6 +61,7 @@ "@types/jest": "^29.5.11", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", + "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "eslint": "^8.45.0", "eslint-config-airbnb-base": "^15.0.0", diff --git a/packages/sdk/svelte/src/lib/LDFlag.svelte b/packages/sdk/svelte/src/lib/LDFlag.svelte index 230e78908d..f0d4bfe20e 100644 --- a/packages/sdk/svelte/src/lib/LDFlag.svelte +++ b/packages/sdk/svelte/src/lib/LDFlag.svelte @@ -1,8 +1,8 @@ diff --git a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts index 2dbdc84ca3..21fc32ffb5 100644 --- a/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts +++ b/packages/sdk/svelte/src/lib/client/SvelteLDClient.ts @@ -1,4 +1,4 @@ -import { derived, get, type Readable, readonly, writable, type Writable } from 'svelte/store'; +import { derived, type Readable, readonly, writable, type Writable } from 'svelte/store'; import type { LDFlagSet } from '@launchdarkly/js-client-sdk'; import { @@ -8,7 +8,7 @@ import { type LDFlagValue, } from '@launchdarkly/js-client-sdk/compat'; -export type { LDContext }; +export type { LDContext, LDFlagValue }; /** Client ID for LaunchDarkly */ export type LDClientID = string; @@ -112,15 +112,15 @@ function createLD() { derived, LDFlagValue>(flagsWritable, ($flags) => $flags[flagKey]); /** - * Checks if a flag is on. - * @param {string} flagKey - The key of the flag to check. - * @returns {boolean} True if the flag is on, false otherwise. + * Gets the current value of a flag. + * @param {string} flagKey - The key of the flag to get. + * @param {TFlag} defaultValue - The default value of the flag. + * @returns {TFlag} The current value of the flag. */ - const isOn = (flagKey: string): boolean => { + function useFlag(flagKey: string, defaultValue: TFlag): TFlag { isClientInitialized(coreLdClient); - const currentFlags = get(flagsWritable); - return !!currentFlags[flagKey]; - }; + return coreLdClient.variation(flagKey, defaultValue); + } return { identify, @@ -128,7 +128,7 @@ function createLD() { initialize: LDInitialize, initializing: readonly(loading), watch, - isOn, + useFlag, }; } From a3711fc4805b1fec89b85609451cb89f4e57ded7 Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Tue, 10 Dec 2024 21:42:36 -0300 Subject: [PATCH 10/12] docs: update README files to reflect SDK client ID changes and improve error handling in example --- packages/sdk/svelte/README.md | 48 +++++++++---------- packages/sdk/svelte/example/README.md | 2 +- .../svelte/example/src/routes/+layout.svelte | 13 ++++- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/sdk/svelte/README.md b/packages/sdk/svelte/README.md index 55671a5ad8..c8d7def68e 100644 --- a/packages/sdk/svelte/README.md +++ b/packages/sdk/svelte/README.md @@ -17,14 +17,14 @@ This is a Svelte library for Launch Darkly. It is a wrapper around the official First, install the package: ```bash -npm install launchdarkly-svelte-client-sdk # or use yarn or pnpm +npm install @launchdarkly/svelte-client-sdk # or use yarn or pnpm ``` Then, initialize the SDK with your client-side ID using the `LDProvider` component: ```svelte @@ -35,7 +35,7 @@ const context = { }, }; - + ``` @@ -44,16 +44,16 @@ Now you can use the `LDFlag` component to conditionally render content based on ```svelte -
-

this will render if the feature flag is on

-
-
-

this will render if the feature flag is off

-
+
+

this will render if the feature flag is on

+
+
+

this will render if the feature flag is off

+
``` @@ -65,7 +65,7 @@ You can change the user context by using the `identify` function from the `LD` o ```svelte ``` -**Note:** Please note that `isOn` function will return the current value of the flag at the time of evaluation, which means you won't get notified if the flag value changes. This is useful for cases where you need to get the value of a flag at a specific time like a function call. If you need to get notified when the flag value changes, you should use the `LDFlag` component, the `watch` function or the `flags` object depending on you use case. +**Note:** Please note that `useFlag` function will return the current value of the flag at the time of evaluation, which means you won't get notified if the flag value changes. This is useful for cases where you need to get the value of a flag at a specific time like a function call. If you need to get notified when the flag value changes, you should use the `LDFlag` component, the `watch` function or the `flags` object depending on your use case. #### Watching flag value changes @@ -102,9 +102,9 @@ If you need to get notified when a flag value changes you can use the `watch` fu ```svelte

{$flagValue}

@@ -116,16 +116,16 @@ If you need to get all flag values you can use the `flags` object. The `flags` o ```svelte {#each Object.keys($allFlags) as flagName} -

{flagName}: {$allFlags[flagName]}

+

{flagName}: {$allFlags[flagName]}

{/each} ``` ## Credits -- Original code by [Robinson Marquez](https://github.com/nosnibor89) +- Original code by [Robinson Marquez](https://github.com/nosnibor89) \ No newline at end of file diff --git a/packages/sdk/svelte/example/README.md b/packages/sdk/svelte/example/README.md index c942c17c90..173a3cb6c8 100644 --- a/packages/sdk/svelte/example/README.md +++ b/packages/sdk/svelte/example/README.md @@ -13,7 +13,7 @@ yarn install Next, create a `.env` file in the root of the project and add your LaunchDarkly client-side ID and flag key. You can obtain these from any LaunchDarkly project/environment you choose. ```bash -PUBLIC_LD_CLIENT_SIDE_ID=your-client-side-id +PUBLIC_LD_CLIENT_ID=your-client-side-id PUBLIC_LD_FLAG_KEY=your-flag-key ``` diff --git a/packages/sdk/svelte/example/src/routes/+layout.svelte b/packages/sdk/svelte/example/src/routes/+layout.svelte index 2229218176..a9edddd3aa 100644 --- a/packages/sdk/svelte/example/src/routes/+layout.svelte +++ b/packages/sdk/svelte/example/src/routes/+layout.svelte @@ -9,10 +9,19 @@ }; -
+ +{#snippet failed(error: unknown, reset: () => void)} +
+

Something failed!

+

There was an error loading the app. Please make sure you have the environment variables properly setup

+ +
+{/snippet} + +

loading flags...

-
+ From 5259138e83434231281177d3641d006de31261b2 Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Mon, 24 Feb 2025 15:04:15 -0300 Subject: [PATCH 11/12] fix: Correct export syntax for LDClient in Svelte SDK --- packages/sdk/svelte/src/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/svelte/src/lib/index.ts b/packages/sdk/svelte/src/lib/index.ts index 32c2c870bb..98be973de8 100644 --- a/packages/sdk/svelte/src/lib/index.ts +++ b/packages/sdk/svelte/src/lib/index.ts @@ -1,5 +1,5 @@ // Reexport your entry components here -export * as LDClient from './client/SvelteLDClient.js'; +export { LD as LDClient } from './client/SvelteLDClient.js'; // Export Components export { default as LDProvider } from './provider/LDProvider.svelte'; From d355338ed28cb0312aceb6564fd08372b46f90cc Mon Sep 17 00:00:00 2001 From: Robinson Marquez Date: Mon, 24 Feb 2025 15:17:04 -0300 Subject: [PATCH 12/12] fix: Update export for LDClient in Svelte SDK --- packages/sdk/svelte/src/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/svelte/src/lib/index.ts b/packages/sdk/svelte/src/lib/index.ts index 98be973de8..ed62c5043d 100644 --- a/packages/sdk/svelte/src/lib/index.ts +++ b/packages/sdk/svelte/src/lib/index.ts @@ -1,5 +1,5 @@ // Reexport your entry components here -export { LD as LDClient } from './client/SvelteLDClient.js'; +export { LD } from './client/SvelteLDClient.js'; // Export Components export { default as LDProvider } from './provider/LDProvider.svelte';