From 0e563656f7743f10c514d9640705e964d7330031 Mon Sep 17 00:00:00 2001 From: Zachary Friss Date: Wed, 21 Jan 2026 14:57:10 -0500 Subject: [PATCH 1/3] feat: inject SDK version at build time and add prefetch subpath export - Replace package.json require with __SDK_VERSION__ build-time constant - Add separate prefetch entry point for smaller bundle imports - Enable code splitting in tsup for tree-shaking - Add ./prefetch subpath export in package.json - Full bundle: ~14.8KB, Prefetch-only: ~3.2KB (78% smaller) --- index.ts | 7 +++-- jest.config.ts | 7 ++++- package.json | 12 +++++--- src/prefetch.test.ts | 58 +++++++++++++++++++++++++++++++++-- src/prefetch.ts | 45 +++++++++++++++++++++++++++ src/reforge.ts | 72 ++++++++++++++++++-------------------------- tsconfig.json | 2 +- tsup.config.ts | 14 ++++++--- yarn.lock | 18 ----------- 9 files changed, 160 insertions(+), 75 deletions(-) create mode 100644 src/prefetch.ts diff --git a/index.ts b/index.ts index 9373cec..1d24dad 100644 --- a/index.ts +++ b/index.ts @@ -8,8 +8,11 @@ import { import { Config } from "./src/config"; import Context from "./src/context"; import { LogLevel, getLogLevelSeverity, shouldLogAtLevel } from "./src/logger"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { version } = require("./package.json"); + +/* eslint-disable no-underscore-dangle */ +declare const __SDK_VERSION__: string; +const version = __SDK_VERSION__; +/* eslint-enable no-underscore-dangle */ export { reforge, diff --git a/jest.config.ts b/jest.config.ts index f908e7a..0f31ddc 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -5,6 +5,9 @@ import type { Config } from "jest"; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require("./package.json"); + const config: Config = { // All imported modules in your tests should be mocked automatically // automock: false, @@ -67,7 +70,9 @@ const config: Config = { // globalTeardown: undefined, // A set of global variables that need to be available in all test environments - // globals: {}, + globals: { + __SDK_VERSION__: packageJson.version, + }, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", diff --git a/package.json b/package.json index bc811fc..51a6ac9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@types/eslint-plugin-jsx-a11y": "^6", "@types/express": "^4.17.13", "@types/jest": "^28.1.6", - "@types/uuid": "^9.0.5", "@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/parser": "^5.33.0", "esbuild": "^0.25.11", @@ -55,14 +54,17 @@ "url": "https://github.com/ReforgeHQ/sdk-javascript/issues" }, "homepage": "https://github.com/ReforgeHQ/sdk-javascript#readme", - "dependencies": { - "uuid": "^9.0.1" - }, "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" + }, + "./prefetch": { + "types": "./dist/src/prefetch.d.ts", + "import": "./dist/src/prefetch.mjs", + "require": "./dist/src/prefetch.cjs" } - } + }, + "sideEffects": false } diff --git a/src/prefetch.test.ts b/src/prefetch.test.ts index b5b3960..ce9a1af 100644 --- a/src/prefetch.test.ts +++ b/src/prefetch.test.ts @@ -1,17 +1,23 @@ /** * @jest-environment jsdom */ -import { prefetchReforgeConfig, Reforge } from "./reforge"; -import Context from "./context"; +import { prefetchReforgeConfig, Context } from "./prefetch"; +import { Reforge } from "./reforge"; describe("prefetchReforgeConfig", () => { const sdkKey = "test-sdk-key"; const context = new Context({ user: { id: "123" } }); + let consoleWarnSpy: jest.SpyInstance; beforeEach(() => { // Reset window object (window as any).REFORGE_SDK_PREFETCH_PROMISE = undefined; jest.clearAllMocks(); + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { }); + }); + + afterEach(() => { + consoleWarnSpy.mockRestore(); }); it("should set REFORGE_SDK_PREFETCH_PROMISE on window", () => { @@ -73,4 +79,52 @@ describe("prefetchReforgeConfig", () => { // Verify window global was cleared (as per loader logic) expect((window as any).REFORGE_SDK_PREFETCH_PROMISE).toBeUndefined(); }); + + it("should not warn when calling get after data is loaded", async () => { + // Mock fetch with actual config data + global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + evaluations: { + "test-flag": { + value: { bool: true }, + configEvaluationMetadata: { + configRowIndex: "0", + conditionalValueIndex: "0", + type: "bool", + id: "123", + }, + }, + }, + }), + } as Response) + ); + + // Start prefetch + prefetchReforgeConfig({ sdkKey, context }); + + // Initialize Reforge and wait for it to load + const reforgeInstance = new Reforge(); + await reforgeInstance.init({ sdkKey, context }); + + // Call get after data is loaded - should NOT warn + const value = reforgeInstance.get("test-flag"); + + expect(value).toBe(true); + expect(consoleWarnSpy).not.toHaveBeenCalled(); + }); + + it("should warn when calling get before data is loaded", () => { + const reforgeInstance = new Reforge(); + + // Call get before init - should warn + const value = reforgeInstance.get("some-flag"); + + expect(value).toBeUndefined(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + expect.stringContaining("The client has not finished loading data yet") + ); + }); }); diff --git a/src/prefetch.ts b/src/prefetch.ts new file mode 100644 index 0000000..16af15b --- /dev/null +++ b/src/prefetch.ts @@ -0,0 +1,45 @@ +import Context from "./context"; +import Loader, { CollectContextModeType } from "./loader"; + +// Re-export Context so consumers can create contexts for prefetch +export { Context }; + +/* eslint-disable no-underscore-dangle */ +declare const __SDK_VERSION__: string; +const version = __SDK_VERSION__; +/* eslint-enable no-underscore-dangle */ + +export type PrefetchParams = { + sdkKey: string; + context: Context; + endpoints?: string[] | undefined; + timeout?: number; + collectContextMode?: CollectContextModeType; + clientNameString?: string; + clientVersionString?: string; +}; + +export function prefetchReforgeConfig({ + sdkKey, + context, + endpoints = undefined, + timeout = undefined, + collectContextMode = "PERIODIC_EXAMPLE", + clientNameString = "sdk-javascript", + clientVersionString = version, +}: PrefetchParams) { + const clientNameAndVersionString = `${clientNameString}-${clientVersionString}`; + + const loader = new Loader({ + sdkKey, + context, + endpoints, + timeout, + collectContextMode, + clientVersion: clientNameAndVersionString, + }); + + (window as any).REFORGE_SDK_PREFETCH_PROMISE = loader.load(); +} + +export default prefetchReforgeConfig; diff --git a/src/reforge.ts b/src/reforge.ts index 443a683..f86633e 100644 --- a/src/reforge.ts +++ b/src/reforge.ts @@ -1,6 +1,4 @@ /* eslint-disable max-classes-per-file */ -import { v4 as uuid } from "uuid"; - import { Config, EvaluationPayload, RawConfigWithoutTypes } from "./config"; import type { Duration, @@ -20,8 +18,19 @@ import { } from "./logger"; import TelemetryUploader from "./telemetryUploader"; import { LoggerAggregator } from "./loggerAggregator"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { version } = require("../package.json"); + +/* eslint-disable no-underscore-dangle */ +declare const __SDK_VERSION__: string; +const version = __SDK_VERSION__; +/* eslint-enable no-underscore-dangle */ + +function uuid() { + if (typeof crypto !== "undefined") { + return crypto.randomUUID(); + } + + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); +} type EvaluationCallback = ( key: K, @@ -150,7 +159,7 @@ export class Reforge { public loader: Loader | undefined; - public afterEvaluationCallback = (() => {}) as EvaluationCallback; + public afterEvaluationCallback = (() => { }) as EvaluationCallback; private _context: Context = new Context({}); @@ -168,7 +177,7 @@ export class Reforge { endpoints = undefined, apiEndpoint, timeout = undefined, - afterEvaluationCallback = () => {}, + afterEvaluationCallback = () => { }, collectEvaluationSummaries = true, collectLoggerNames = false, collectContextMode = "PERIODIC_EXAMPLE", @@ -368,12 +377,12 @@ export class Reforge { // We need to calcuate these live and not store in a type to ensure dynamic evaluation // in upstream libraries that override the FrontEndConfigurationRaw interface K extends keyof FrontEndConfigurationRaw extends never - ? string - : { - [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends boolean - ? IK - : never; - }[keyof TypedFrontEndConfigurationRaw], + ? string + : { + [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends boolean + ? IK + : never; + }[keyof TypedFrontEndConfigurationRaw], >(key: K): boolean { return this.get(key) === true; } @@ -409,12 +418,12 @@ export class Reforge { // We need to calcuate these live and not store in a type to ensure dynamic evaluation // in upstream libraries that override the FrontEndConfigurationRaw interface K extends keyof FrontEndConfigurationRaw extends never - ? string - : { - [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends Duration - ? IK - : never; - }[keyof TypedFrontEndConfigurationRaw], + ? string + : { + [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends Duration + ? IK + : never; + }[keyof TypedFrontEndConfigurationRaw], >(key: K): Duration | undefined { const value = this.get(key); @@ -470,27 +479,6 @@ export class Reforge { export const reforge = new Reforge(); -export function prefetchReforgeConfig({ - sdkKey, - context, - endpoints = undefined, - timeout = undefined, - collectContextMode = "PERIODIC_EXAMPLE", - clientNameString = "sdk-javascript", - clientVersionString = version, -}: ReforgeInitParams) { - const clientNameAndVersionString = `${clientNameString}-${clientVersionString}`; - - const loader = new Loader({ - sdkKey, - context, - endpoints, - timeout, - collectContextMode, - clientVersion: clientNameAndVersionString, - }); - - (window as any).REFORGE_SDK_PREFETCH_PROMISE = loader.load(); -} - -export default prefetchReforgeConfig; +// Re-export prefetchReforgeConfig for backwards compatibility +export { prefetchReforgeConfig } from "./prefetch"; +export type { PrefetchParams } from "./prefetch"; diff --git a/tsconfig.json b/tsconfig.json index 33c3f11..cdc1d3a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ diff --git a/tsup.config.ts b/tsup.config.ts index b43e9cb..5f4346e 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,17 +1,23 @@ import { defineConfig } from "tsup"; import { execSync } from "child_process"; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const packageJson = require("./package.json"); + export default defineConfig({ - entry: ["index.ts"], + entry: ["index.ts", "src/prefetch.ts"], format: ["cjs", "esm"], // Build both CommonJS and ESM versions dts: true, // Generate declaration files - splitting: false, + splitting: true, // Enable code splitting for smaller imports sourcemap: true, clean: true, // Clean output directory before build - minify: false, - external: ["uuid"], // Don't bundle uuid + minify: true, + external: [], noExternal: [], outDir: "dist", + define: { + __SDK_VERSION__: JSON.stringify(packageJson.version), + }, outExtension: ({ format }) => ({ js: format === "cjs" ? ".cjs" : ".mjs", }), diff --git a/yarn.lock b/yarn.lock index 2b8dba9..4fbcfd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1295,7 +1295,6 @@ __metadata: "@types/eslint-plugin-jsx-a11y": "npm:^6" "@types/express": "npm:^4.17.13" "@types/jest": "npm:^28.1.6" - "@types/uuid": "npm:^9.0.5" "@typescript-eslint/eslint-plugin": "npm:^5.33.0" "@typescript-eslint/parser": "npm:^5.33.0" esbuild: "npm:^0.25.11" @@ -1315,7 +1314,6 @@ __metadata: ts-node: "npm:^10.9.1" tsup: "npm:^8.0.2" typescript: "npm:^5.1.6" - uuid: "npm:^9.0.1" languageName: unknown linkType: soft @@ -1813,13 +1811,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.5": - version: 9.0.8 - resolution: "@types/uuid@npm:9.0.8" - checksum: 10c0/b411b93054cb1d4361919579ef3508a1f12bf15b5fdd97337d3d351bece6c921b52b6daeef89b62340fd73fd60da407878432a1af777f40648cbe53a01723489 - languageName: node - linkType: hard - "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -7580,15 +7571,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" - bin: - uuid: dist/bin/uuid - checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b - languageName: node - linkType: hard - "v8-compile-cache-lib@npm:^3.0.1": version: 3.0.1 resolution: "v8-compile-cache-lib@npm:3.0.1" From c8e977674c768f27af78ca3bea6b1f47ef4b1ffd Mon Sep 17 00:00:00 2001 From: Zachary Friss Date: Wed, 21 Jan 2026 14:59:58 -0500 Subject: [PATCH 2/3] docs: add lightweight prefetch import to README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d01e4e..3be3306 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ To avoid a request waterfall, you can start fetching the configuration early in lifecycle, before the React SDK or `reforge.init()` is called. ```javascript -import { prefetchReforgeConfig, Context } from "@reforge-com/javascript"; +import { prefetchReforgeConfig, Context } from "@reforge-com/javascript/prefetch"; prefetchReforgeConfig({ sdkKey: "1234", @@ -83,7 +83,7 @@ prefetchReforgeConfig({ }); ``` -When you later call `reforge.init()`, it will automatically use the prefetched promise if available. +This lightweight import (~3KB) is ideal for early loading. When you later call `reforge.init()`, it will automatically use the prefetched promise if available. ## Client API From deb8666b77324def3a6fcc6758a3297bb7590ec6 Mon Sep 17 00:00:00 2001 From: Zachary Friss Date: Wed, 21 Jan 2026 15:04:03 -0500 Subject: [PATCH 3/3] style: run prettier --- README.md | 3 ++- src/prefetch.test.ts | 2 +- src/reforge.ts | 28 ++++++++++++++-------------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3be3306..1d9e0d3 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,8 @@ prefetchReforgeConfig({ }); ``` -This lightweight import (~3KB) is ideal for early loading. When you later call `reforge.init()`, it will automatically use the prefetched promise if available. +This lightweight import (~3KB) is ideal for early loading. When you later call `reforge.init()`, it +will automatically use the prefetched promise if available. ## Client API diff --git a/src/prefetch.test.ts b/src/prefetch.test.ts index ce9a1af..10c509f 100644 --- a/src/prefetch.test.ts +++ b/src/prefetch.test.ts @@ -13,7 +13,7 @@ describe("prefetchReforgeConfig", () => { // Reset window object (window as any).REFORGE_SDK_PREFETCH_PROMISE = undefined; jest.clearAllMocks(); - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => { }); + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); }); afterEach(() => { diff --git a/src/reforge.ts b/src/reforge.ts index f86633e..220d298 100644 --- a/src/reforge.ts +++ b/src/reforge.ts @@ -159,7 +159,7 @@ export class Reforge { public loader: Loader | undefined; - public afterEvaluationCallback = (() => { }) as EvaluationCallback; + public afterEvaluationCallback = (() => {}) as EvaluationCallback; private _context: Context = new Context({}); @@ -177,7 +177,7 @@ export class Reforge { endpoints = undefined, apiEndpoint, timeout = undefined, - afterEvaluationCallback = () => { }, + afterEvaluationCallback = () => {}, collectEvaluationSummaries = true, collectLoggerNames = false, collectContextMode = "PERIODIC_EXAMPLE", @@ -377,12 +377,12 @@ export class Reforge { // We need to calcuate these live and not store in a type to ensure dynamic evaluation // in upstream libraries that override the FrontEndConfigurationRaw interface K extends keyof FrontEndConfigurationRaw extends never - ? string - : { - [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends boolean - ? IK - : never; - }[keyof TypedFrontEndConfigurationRaw], + ? string + : { + [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends boolean + ? IK + : never; + }[keyof TypedFrontEndConfigurationRaw], >(key: K): boolean { return this.get(key) === true; } @@ -418,12 +418,12 @@ export class Reforge { // We need to calcuate these live and not store in a type to ensure dynamic evaluation // in upstream libraries that override the FrontEndConfigurationRaw interface K extends keyof FrontEndConfigurationRaw extends never - ? string - : { - [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends Duration - ? IK - : never; - }[keyof TypedFrontEndConfigurationRaw], + ? string + : { + [IK in keyof TypedFrontEndConfigurationRaw]: TypedFrontEndConfigurationRaw[IK] extends Duration + ? IK + : never; + }[keyof TypedFrontEndConfigurationRaw], >(key: K): Duration | undefined { const value = this.get(key);