From 258a67b940cc21e9793b40bfb3aeb2c18ebe7543 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Fri, 18 Sep 2026 16:27:36 -0400 Subject: [PATCH 01/14] feat: Add an opt-in fetch-based EventSource entry to the browser SDK --- package.json | 1 + .../BrowserClient.eventSource.test.ts | 65 +++++++++++ .../sdk/browser/__tests__/options.test.ts | 39 +++++++ .../platform/BrowserPlatform.test.ts | 42 ++++++- .../platform/BrowserRequests.test.ts | 69 ++++++++++++ .../platform/fetchBrowserEventSource.test.ts | 56 +++++++++ .../features/custom-eventsource/.env.template | 5 + .../features/custom-eventsource/README.md | 78 +++++++++++++ .../features/custom-eventsource/index.css | 11 ++ .../features/custom-eventsource/index.html | 11 ++ .../features/custom-eventsource/package.json | 25 +++++ .../features/custom-eventsource/src/app.ts | 106 ++++++++++++++++++ .../features/custom-eventsource/tsconfig.json | 22 ++++ .../custom-eventsource/tsdown.config.ts | 40 +++++++ packages/sdk/browser/package.json | 20 ++++ packages/sdk/browser/src/BrowserClient.ts | 4 +- packages/sdk/browser/src/common.ts | 4 + packages/sdk/browser/src/fetch-eventsource.ts | 14 +++ packages/sdk/browser/src/options.ts | 25 +++++ .../browser/src/platform/BrowserPlatform.ts | 11 +- .../browser/src/platform/BrowserRequests.ts | 35 +++++- .../src/platform/fetchBrowserEventSource.ts | 46 ++++++++ packages/sdk/browser/tsup.config.ts | 5 + release-please-config.json | 5 + 24 files changed, 729 insertions(+), 10 deletions(-) create mode 100644 packages/sdk/browser/__tests__/BrowserClient.eventSource.test.ts create mode 100644 packages/sdk/browser/__tests__/platform/BrowserRequests.test.ts create mode 100644 packages/sdk/browser/__tests__/platform/fetchBrowserEventSource.test.ts create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/.env.template create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/README.md create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/index.css create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/index.html create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/package.json create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/src/app.ts create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/tsconfig.json create mode 100644 packages/sdk/browser/examples/features/custom-eventsource/tsdown.config.ts create mode 100644 packages/sdk/browser/src/fetch-eventsource.ts create mode 100644 packages/sdk/browser/src/platform/fetchBrowserEventSource.ts diff --git a/package.json b/package.json index 1a66c54eb2..e0ad23a433 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "packages/sdk/shopify-oxygen/example", "packages/sdk/browser/example", "packages/sdk/browser/example-fdv2", + "packages/sdk/browser/examples/features/custom-eventsource", "packages/sdk/openfeature-node-server", "packages/sdk/openfeature-node-server/examples/getting-started", "packages/sdk/openfeature-cloudflare-server", diff --git a/packages/sdk/browser/__tests__/BrowserClient.eventSource.test.ts b/packages/sdk/browser/__tests__/BrowserClient.eventSource.test.ts new file mode 100644 index 0000000000..8c992cb80b --- /dev/null +++ b/packages/sdk/browser/__tests__/BrowserClient.eventSource.test.ts @@ -0,0 +1,65 @@ +import { AutoEnvAttributes, LDLogger } from '@launchdarkly/js-client-sdk-common'; + +import { makeClient } from '../src/BrowserClient'; + +const mockPlatformConstructor = jest.fn(); + +jest.mock('../src/platform/BrowserPlatform', () => ({ + __esModule: true, + default: jest.fn().mockImplementation((logger, options, storage, eventSourceFactory) => { + mockPlatformConstructor(logger, options, storage, eventSourceFactory); + const { makeBasicPlatform } = require('./BrowserClient.mocks'); + const platform = makeBasicPlatform(options); + return platform; + }), +})); + +let logger: LDLogger; + +beforeEach(() => { + jest.clearAllMocks(); + logger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() }; +}); + +function getWiredEventSource() { + return mockPlatformConstructor.mock.calls[0][3]; +} + +it('passes a valid event source factory through to the platform unchanged', () => { + const eventSource = { createEventSource: jest.fn() }; + + makeClient('client-side-id', { key: 'user-key', kind: 'user' }, AutoEnvAttributes.Disabled, { + streaming: false, + logger, + diagnosticOptOut: true, + eventSource, + }); + + expect(getWiredEventSource()).toBe(eventSource); + expect(logger.warn).not.toHaveBeenCalled(); +}); + +it('does not pass an event source factory when none is configured', () => { + makeClient('client-side-id', { key: 'user-key', kind: 'user' }, AutoEnvAttributes.Disabled, { + streaming: false, + logger, + diagnosticOptOut: true, + }); + + expect(getWiredEventSource()).toBeUndefined(); +}); + +it('drops an invalid event source factory and warns', () => { + makeClient('client-side-id', { key: 'user-key', kind: 'user' }, AutoEnvAttributes.Disabled, { + streaming: false, + logger, + diagnosticOptOut: true, + // @ts-ignore + eventSource: 'not an object', + }); + + expect(getWiredEventSource()).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + 'Config option "eventSource" should be of type object, got string, using default value', + ); +}); diff --git a/packages/sdk/browser/__tests__/options.test.ts b/packages/sdk/browser/__tests__/options.test.ts index 59ca837a1a..b30032fac0 100644 --- a/packages/sdk/browser/__tests__/options.test.ts +++ b/packages/sdk/browser/__tests__/options.test.ts @@ -119,3 +119,42 @@ it('passes an empty dataSystem through to base options', () => { const result = filterToBaseOptionsWithDefaults(opts); expect((result as any).dataSystem).toEqual({}); }); + +it('accepts an event source factory', () => { + const eventSource = { createEventSource: jest.fn() } as unknown as NonNullable< + Parameters[0]['eventSource'] + >; + + const opts = validateBrowserOptions({ eventSource }, logger); + + expect(opts.eventSource).toBe(eventSource); + expect(logger.warn).not.toHaveBeenCalled(); +}); + +it('warns and drops an event source that is not an object', () => { + const opts = validateBrowserOptions( + { + // @ts-ignore + eventSource: 'not an object', + }, + logger, + ); + + expect(opts.eventSource).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledWith( + 'Config option "eventSource" should be of type object, got string, using default value', + ); +}); + +it('defaults the event source factory to undefined', () => { + const opts = validateBrowserOptions({}, logger); + expect(opts.eventSource).toBeUndefined(); +}); + +it('strips the event source factory from base options', () => { + const eventSource = { createEventSource: jest.fn() } as unknown as NonNullable< + Parameters[0]['eventSource'] + >; + const baseOpts = filterToBaseOptionsWithDefaults({ eventSource }); + expect(baseOpts).not.toHaveProperty('eventSource'); +}); diff --git a/packages/sdk/browser/__tests__/platform/BrowserPlatform.test.ts b/packages/sdk/browser/__tests__/platform/BrowserPlatform.test.ts index f86bb4018e..32887a853d 100644 --- a/packages/sdk/browser/__tests__/platform/BrowserPlatform.test.ts +++ b/packages/sdk/browser/__tests__/platform/BrowserPlatform.test.ts @@ -1,4 +1,9 @@ -import { LDLogger, Storage } from '@launchdarkly/js-client-sdk-common'; +import { + EventSourceInitDict, + LDEventSourceFactory, + LDLogger, + Storage, +} from '@launchdarkly/js-client-sdk-common'; import BrowserPlatform from '../../src/platform/BrowserPlatform'; import LocalStorage, { isLocalStorageSupported } from '../../src/platform/LocalStorage'; @@ -63,3 +68,38 @@ it('uses the provided storage override even when localStorage is unsupported', ( expect(platform.storage).toBe(custom); expect(mockLocalStorage).not.toHaveBeenCalled(); }); + +it('reports no optional event source capabilities when no factory override is provided', () => { + const platform = new BrowserPlatform(logger, {}); + + expect(platform.requests.getEventSourceCapabilities()).toEqual({ + customMethod: false, + readTimeout: false, + headers: false, + }); +}); + +it('uses the provided event source factory override', () => { + const created = { close: jest.fn() }; + const factory: LDEventSourceFactory = { + createEventSource: jest.fn().mockReturnValue(created), + capabilities: { customMethod: true, readTimeout: true, headers: true }, + }; + + const platform = new BrowserPlatform(logger, {}, undefined, factory); + const initDict: EventSourceInitDict = { + headers: { authorization: 'sdk-key' }, + errorFilter: () => true, + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + }; + + expect(platform.requests.createEventSource('http://example.com/stream', initDict)).toBe(created); + expect(factory.createEventSource).toHaveBeenCalledWith('http://example.com/stream', initDict); + expect(platform.requests.getEventSourceCapabilities()).toEqual({ + customMethod: true, + readTimeout: true, + headers: true, + }); +}); diff --git a/packages/sdk/browser/__tests__/platform/BrowserRequests.test.ts b/packages/sdk/browser/__tests__/platform/BrowserRequests.test.ts new file mode 100644 index 0000000000..47ac30658a --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/BrowserRequests.test.ts @@ -0,0 +1,69 @@ +import { EventSourceInitDict, LDEventSourceFactory } from '@launchdarkly/js-client-sdk-common'; + +import BrowserRequests from '../../src/platform/BrowserRequests'; +import DefaultBrowserEventSource from '../../src/platform/DefaultBrowserEventSource'; + +const initDict: EventSourceInitDict = { + headers: { authorization: 'sdk-key' }, + errorFilter: () => true, + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, +}; + +beforeEach(() => { + // jsdom does not implement EventSource, and DefaultBrowserEventSource constructs one as soon as + // it is created. + // @ts-ignore + globalThis.EventSource = jest.fn(() => ({ + addEventListener: jest.fn(), + close: jest.fn(), + })); +}); + +it('reports no optional capabilities by default', () => { + const requests = new BrowserRequests(); + + expect(requests.getEventSourceCapabilities()).toEqual({ + customMethod: false, + readTimeout: false, + headers: false, + }); +}); + +it('creates a native browser event source by default', () => { + const requests = new BrowserRequests(); + + const es = requests.createEventSource('http://example.com/stream', initDict); + + expect(es).toBeInstanceOf(DefaultBrowserEventSource); + es.close(); +}); + +it('uses an injected factory and its declared capabilities', () => { + const created = { close: jest.fn() }; + const factory: LDEventSourceFactory = { + createEventSource: jest.fn().mockReturnValue(created), + capabilities: { customMethod: true, readTimeout: true, headers: true }, + }; + const requests = new BrowserRequests(factory); + + expect(requests.createEventSource('http://example.com/stream', initDict)).toBe(created); + expect(factory.createEventSource).toHaveBeenCalledWith('http://example.com/stream', initDict); + expect(requests.getEventSourceCapabilities()).toEqual({ + customMethod: true, + readTimeout: true, + headers: true, + }); +}); + +it('reports no optional capabilities for a factory that declares none', () => { + const factory: LDEventSourceFactory = { createEventSource: jest.fn() }; + const requests = new BrowserRequests(factory); + + expect(requests.getEventSourceCapabilities()).toEqual({ + customMethod: false, + readTimeout: false, + headers: false, + }); +}); diff --git a/packages/sdk/browser/__tests__/platform/fetchBrowserEventSource.test.ts b/packages/sdk/browser/__tests__/platform/fetchBrowserEventSource.test.ts new file mode 100644 index 0000000000..f9e1fb862a --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/fetchBrowserEventSource.test.ts @@ -0,0 +1,56 @@ +import { createEventSource as mockedCreateEventSource } from '@launchdarkly/eventsource'; +import { EventSourceInitDict } from '@launchdarkly/js-client-sdk-common'; + +import fetchBrowserEventSource from '../../src/platform/fetchBrowserEventSource'; + +// The fetch-based EventSource needs a streaming fetch, which jsdom does not provide. Only the +// adapter is under test here; the implementation itself is covered by the eventsource package's +// own suite. +jest.mock('@launchdarkly/eventsource', () => ({ + createEventSource: jest.fn(), +})); + +const mockCreateEventSource = mockedCreateEventSource as unknown as jest.Mock; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('declares support for headers, a custom method, and a read timeout', () => { + expect(fetchBrowserEventSource.capabilities).toEqual({ + customMethod: true, + readTimeout: true, + headers: true, + }); +}); + +it('passes the url and every option through, plus the backoff defaults', () => { + const urlBuilder = () => 'http://example.com/stream?basis=2'; + const errorFilter = () => true; + const initDict: EventSourceInitDict = { + method: 'REPORT', + headers: { authorization: 'sdk-key' }, + body: '{"kind":"user"}', + errorFilter, + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + urlBuilder, + }; + + fetchBrowserEventSource.createEventSource('http://example.com/stream', initDict); + + expect(mockCreateEventSource).toHaveBeenCalledTimes(1); + expect(mockCreateEventSource).toHaveBeenCalledWith('http://example.com/stream', { + method: 'REPORT', + headers: { authorization: 'sdk-key' }, + body: '{"kind":"user"}', + errorFilter, + initialRetryDelayMillis: 1000, + readTimeoutMillis: 300000, + retryResetIntervalMillis: 60000, + urlBuilder, + maxBackoffMillis: 30000, + jitterRatio: 0.5, + }); +}); diff --git a/packages/sdk/browser/examples/features/custom-eventsource/.env.template b/packages/sdk/browser/examples/features/custom-eventsource/.env.template new file mode 100644 index 0000000000..75b8c17e11 --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/.env.template @@ -0,0 +1,5 @@ +# Set LAUNCHDARKLY_CLIENT_SIDE_ID to your LaunchDarkly client-side ID +LAUNCHDARKLY_CLIENT_SIDE_ID= + +# Set LAUNCHDARKLY_FLAG_KEY to the feature flag key you want to evaluate +LAUNCHDARKLY_FLAG_KEY= diff --git a/packages/sdk/browser/examples/features/custom-eventsource/README.md b/packages/sdk/browser/examples/features/custom-eventsource/README.md new file mode 100644 index 0000000000..75ac04d776 --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/README.md @@ -0,0 +1,78 @@ +# LaunchDarkly Browser SDK custom EventSource example + +A minimal vanilla-TypeScript app that demonstrates the Browser SDK's injectable +`eventSource` option. + +## What it demonstrates + +The Browser SDK defaults to the native browser `EventSource` API for its streaming +connection. That API has no way to send a custom HTTP method or a request body, so setting +`usePost: true` alone throws, because the FDv2 data source requires an EventSource that +can send a custom method. + +This example passes `fetchBrowserEventSource` (from +`@launchdarkly/js-client-sdk/fetch-eventsource`) as the `eventSource` option, in +[`src/app.ts`](./src/app.ts). That option opts into a `fetch()`-based EventSource +implementation which _can_ send a POST request with a body, which is what lets +`usePost` actually take effect. Open your browser's devtools Network tab while running +this example and look for a `POST` request to the streaming endpoint, instead of the +`GET`-based request the default transport would make. + +This example also enables the FDv2 data system (`dataSystem: {}`). `usePost` only applies +to FDv2's streaming synchronizer -- the FDv1 data source uses `useReport` instead, which +this example does not set. + +`fetchBrowserEventSource` lives behind its own entry point specifically so that neither it +nor its dependency (`@launchdarkly/eventsource`) ends up in the SDK's default bundle -- +only an application that imports this path pays for it. `@launchdarkly/eventsource` is an +optional dependency of the SDK. If your install omits optional dependencies (for example +`npm install --omit=optional`), install it directly before you import this entry point. + +## Prerequisites + +Nodejs 20.6.0 or later + +## Build instructions + +1. From the repository root, build the SDK and its dependencies first: + + ```bash + yarn workspaces foreach -pR --topological-dev --from '@launchdarkly/js-client-sdk' run build + ``` + +2. Make a copy of the `.env.template` and name it `.env` + + ``` + cp .env.template .env + ``` + +3. Set the variables in `.env` to your specific LD values + + ``` + # Set LAUNCHDARKLY_CLIENT_SIDE_ID to your LaunchDarkly client-side ID + LAUNCHDARKLY_CLIENT_SIDE_ID= + + # Set LAUNCHDARKLY_FLAG_KEY to the feature flag key you want to evaluate + LAUNCHDARKLY_FLAG_KEY= + ``` + + > [!NOTE] + > Setting these values is equivalent to modifying the `clientSideID` and `flagKey` + > in [app.ts](./src/app.ts). + +4. Install and build the example: + + ```bash + yarn workspace @launchdarkly/browser-example-custom-eventsource build + ``` + +5. Run the example: + ```bash + yarn workspace @launchdarkly/browser-example-custom-eventsource start + ``` + > [!NOTE] + > The `start` script simply runs `open index.html`. If that is not working for you, you + > can open the `index.html` file in a browser for the same results. + +The application will run continuously and react to flag changes in LaunchDarkly, streaming +over the fetch-based EventSource the whole time. diff --git a/packages/sdk/browser/examples/features/custom-eventsource/index.css b/packages/sdk/browser/examples/features/custom-eventsource/index.css new file mode 100644 index 0000000000..90dc2b50f1 --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/index.css @@ -0,0 +1,11 @@ +body { + margin: 0; + background: #373841; + color: white; + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', + 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-align: center; +} diff --git a/packages/sdk/browser/examples/features/custom-eventsource/index.html b/packages/sdk/browser/examples/features/custom-eventsource/index.html new file mode 100644 index 0000000000..b469548c25 --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/index.html @@ -0,0 +1,11 @@ + + + + + + LaunchDarkly custom EventSource example + + + + + diff --git a/packages/sdk/browser/examples/features/custom-eventsource/package.json b/packages/sdk/browser/examples/features/custom-eventsource/package.json new file mode 100644 index 0000000000..4429cb4ffd --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/package.json @@ -0,0 +1,25 @@ +{ + "name": "@launchdarkly/browser-example-custom-eventsource", + "version": "0.0.0", + "private": true, + "description": "LaunchDarkly example for the JavaScript Browser SDK's injectable EventSource", + "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/browser/examples/features/custom-eventsource", + "repository": { + "type": "git", + "url": "https://github.com/launchdarkly/js-core.git" + }, + "license": "Apache-2.0", + "type": "module", + "scripts": { + "start": "open index.html", + "clean": "rm -rf dist dist-static", + "build": "npm run clean && tsdown" + }, + "dependencies": { + "@launchdarkly/js-client-sdk": "4.10.2" + }, + "devDependencies": { + "tsdown": "^0.17.0-beta.4", + "typescript": "^5.9.3" + } +} diff --git a/packages/sdk/browser/examples/features/custom-eventsource/src/app.ts b/packages/sdk/browser/examples/features/custom-eventsource/src/app.ts new file mode 100644 index 0000000000..5dceb8571a --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/src/app.ts @@ -0,0 +1,106 @@ +import { createClient } from '@launchdarkly/js-client-sdk'; +import { fetchBrowserEventSource } from '@launchdarkly/js-client-sdk/fetch-eventsource'; + +// Set clientSideID to your LaunchDarkly client-side ID +const clientSideID = 'LD_CLIENT_SIDE_ID'; + +// Set flagKey to the feature flag key you want to evaluate +const flagKey = 'LD_FLAG_KEY'; + +// Set up the evaluation context. This context should appear on your +// LaunchDarkly contexts dashboard soon after you run the demo. +const context = { + kind: 'user', + key: 'example-user-key', + name: 'Sandy', +}; + +const div = document.createElement('div'); +const statusBox = document.createElement('div'); +const transportBox = document.createElement('div'); + +document.body.appendChild(statusBox); +document.body.appendChild(transportBox); +document.body.appendChild(div); + +div.appendChild(document.createTextNode('No flag evaluations yet')); +statusBox.appendChild(document.createTextNode('Initializing...')); +transportBox.appendChild(document.createTextNode('')); + +const main = async () => { + // The native browser EventSource API has no way to send a custom HTTP method or a request + // body, so it cannot do POST-based streaming -- setting `usePost: true` alone throws if the + // configured EventSource cannot send a custom method. Passing `fetchBrowserEventSource` as the + // `eventSource` option opts into a fetch()-based transport that CAN send POST with a body, + // which is what actually lets `usePost` take effect. Open your browser's devtools Network tab + // and look for a POST request to the streaming endpoint to see this in action. + // + // `eventSource` is plumbed in at the platform Requests level, below both data systems, but + // `usePost` only applies to the FDv2 (`dataSystem`) data source -- FDv1 uses `useReport` instead. + const ldclient = createClient(clientSideID, context, { + usePost: true, + eventSource: fetchBrowserEventSource, + dataSystem: {}, + }); + + const render = () => { + const flagValue = ldclient.variation(flagKey, false); + const label = `The ${flagKey} feature flag evaluates to ${flagValue}.`; + document.body.style.background = flagValue ? '#00844B' : '#373841'; + div.replaceChild(document.createTextNode(label), div.firstChild as Node); + }; + + ldclient.on('error', () => { + statusBox.replaceChild( + document.createTextNode('Error caught in client SDK'), + statusBox.firstChild as Node, + ); + }); + + // Listen for flag changes + ldclient.on('change', () => { + render(); + }); + + ldclient.start(); + + const { status } = await ldclient.waitForInitialization(); + + // Both calls are required to actually open a stream: setStreaming forces the foreground mode + // to 'streaming' instead of the browser default ('one-shot', a single fetch with no ongoing + // connection), and setConnectionMode overrides the resolved mode outright. + ldclient.setStreaming(true); + ldclient.setConnectionMode('streaming'); + + if (status === 'complete') { + statusBox.replaceChild( + document.createTextNode(`Initialized with context: ${JSON.stringify(ldclient.getContext())}`), + statusBox.firstChild as Node, + ); + transportBox.replaceChild( + document.createTextNode( + 'Streaming via the fetch()-based EventSource under FDv2 (dataSystem), with POST enabled by usePost.', + ), + transportBox.firstChild as Node, + ); + } else if (status === 'failed') { + statusBox.replaceChild( + document.createTextNode('Error identifying client'), + statusBox.firstChild as Node, + ); + } else if (status === 'timeout') { + statusBox.replaceChild( + document.createTextNode('Timeout identifying client'), + statusBox.firstChild as Node, + ); + } else { + statusBox.replaceChild( + document.createTextNode('Unknown error identifying client'), + statusBox.firstChild as Node, + ); + } + + render(); +}; + +main(); diff --git a/packages/sdk/browser/examples/features/custom-eventsource/tsconfig.json b/packages/sdk/browser/examples/features/custom-eventsource/tsconfig.json new file mode 100644 index 0000000000..a1aec48ce3 --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "lib": ["ES2017", "dom"], + "module": "ESNext", + "moduleResolution": "node", + "noImplicitOverride": true, + "resolveJsonModule": true, + "rootDir": ".", + "outDir": "dist", + "skipLibCheck": true, + "sourceMap": true, + "inlineSources": true, + "strict": true, + "stripInternal": true, + "target": "ES2017", + "allowJs": true + }, + "include": ["src"] +} diff --git a/packages/sdk/browser/examples/features/custom-eventsource/tsdown.config.ts b/packages/sdk/browser/examples/features/custom-eventsource/tsdown.config.ts new file mode 100644 index 0000000000..e682dbca54 --- /dev/null +++ b/packages/sdk/browser/examples/features/custom-eventsource/tsdown.config.ts @@ -0,0 +1,40 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import fs from 'node:fs'; +import path from 'node:path'; +import { loadEnvFile } from 'node:process'; +import { defineConfig } from 'tsdown'; + +if (fs.existsSync('.env')) { + loadEnvFile('.env'); +} + +const ENTRY_FILE = path.join('src', 'app.ts'); +const OUTPUT_FILE = path.join('dist', 'app.js'); +const { LAUNCHDARKLY_CLIENT_SIDE_ID, LAUNCHDARKLY_FLAG_KEY } = process.env; + +const CLIENT_SIDE_ID_PLACEHOLDER = 'LD_CLIENT_SIDE_ID'; +const FLAG_KEY_PLACEHOLDER = 'LD_FLAG_KEY'; + +export default defineConfig({ + entry: ENTRY_FILE, + platform: 'browser', + outDir: 'dist', + // The fetch-eventsource subpath needs its own entry here -- noExternal matches by exact + // specifier, not by package prefix, so listing only the package root would leave this + // subpath's `import` unbundled in the output, which a plain `